text: /*
   * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
   *  
   * 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.
   * 
   */
  
  define(function (require, exports) {
      "use strict";
  
      var Promise = require("bluebird"),
          Immutable = require("immutable");
  
      var os = require("adapter").os;
  
      var locks = require("../locks"),
          layers = require("js/actions/layers"),
          menu = require("./menu"),
          collection = require("js/util/collection"),
          headlights = require("js/util/headlights"),
          history = require("js/actions/history"),
          policyActions = require("js/actions/policy"),
          dom = require("js/util/dom");
  
      /**
       * Native menu command IDs for Photoshop edit commands.
       * 
       * @const     
       * @private
       * @type {number}
       */
      var CUT_NATIVE_MENU_COMMMAND_ID = 103,
          COPY_NATIVE_MENU_COMMMAND_ID = 104,
          PASTE_NATIVE_MENU_COMMMAND_ID = 105,
          SELECT_ALL_NATIVE_MENU_COMMMAND_ID = 1017;
  
      var LAYER_CLIPBOARD_FORMAT = "com.adobe.photoshop.spaces.design.layers";
  
      /**
       * Execute a native cut command.
       *
       * @private
       * @return {Promise}
       */
      var nativeCut = function () {
          return this.transfer(menu.nativeModal, {
              commandID: CUT_NATIVE_MENU_COMMMAND_ID
          })
          .catch(function () {
              // Ignore errors from menu.nativeModal
          });
      };
      nativeCut.action = {
          modal: true,
          reads: [],
          writes: [],
          transfers: [menu.nativeModal]
      };
  
      /**
       * Execute a native copy command.
       *
       * @private
       * @return {Promise}
       */
      var nativeCopy = function () {
          return this.transfer(menu.nativeModal, {
              commandID: COPY_NATIVE_MENU_COMMMAND_ID
          })
          .catch(function () {
              // Ignore errors from menu.nativeModal
          });
      };
      nativeCopy.action = {
          modal: true,
          reads: [],
          writes: [],
          transfers: [menu.nativeModal]
      };
  
      /**
       * Execute a native paste command.
       *
       * @private
       * @return {Promise}
       */
      var nativePaste = function () {
          // FIXME: this suspend policies hack is for pasting smart object from other sources (e.g. Illustrator).
          // The cause is similar to the place-object menu commands: DS is not receiving "toolModalStateChanged" events
          // until the object is committed.
          // To avoid restoring policies while in a modal state, text in particular, first check the tool store
          // modal state before restoring policies.  Super. Hack.
          var policyStore = this.flux.store("policy"),
              suspendPromise;
  
          if (policyStore.areAllSuspended()) {
              suspendPromise = Promise.resolve();
          } else {
              suspendPromise = this.transfer(policyActions.suspendAllPolicies);
          }
  
          return suspendPromise
              .bind(this)
              .then(function () {
                  return this.transfer(menu.nativeModal, {
                      commandID: PASTE_NATIVE_MENU_COMMMAND_ID,
                      waitForCompletion: true
                  });
              })
              .finally(function () {
                  var toolStore = this.flux.store("tool"),
                      policyStore = this.flux.store("policy");
  
                  if (!toolStore.getModalToolState() && policyStore.areAllSuspended()) {
                      return this.transfer(policyActions.restoreAllPolicies);
                  }
              });
      };
      nativePaste.action = {
          modal: true,
          reads: [],
          writes: [],
          transfers: [menu.nativeModal, policyActions.suspendAllPolicies, policyActions.restoreAllPolicies]
      };
  
      /**
       * Execute a native selectAll command.
       *
       * @private
       * @param {boolean} waitForCompletion Flag for nativeModal
       * @return {Promise}
       */
      var nativeSelectAll = function (waitForCompletion) {
          waitForCompletion = waitForCompletion || false;
  
          return this.transfer(menu.nativeModal, {
              commandID: SELECT_ALL_NATIVE_MENU_COMMMAND_ID,
              waitForCompletion: waitForCompletion
          })
          .catch(function () {
              // Ignore errors from menu.nativeModal
          });
      };
      nativeSelectAll.action = {
          modal: true,
          reads: [],
          writes: [],
          transfers: [menu.nativeModal]
      };
  
      /**
       * Execute either a cut or copy operation, depending on the value of the parameter.
       *
       * @private
       * @param {boolean} cut If true, perform a cut operation; otherwise, a copy.
       * @return {Promise}
       */
      var _cutOrCopy = function (cut) {
          return os.hasKeyboardFocus()
              .bind(this)
              .then(function (cefHasFocus) {
                  var el = window.document.activeElement,
                      data;
  
                  if (cefHasFocus && dom.isInput(el)) {
                      if (dom.isTextInput(el)) {
                          data = el.value.substring(el.selectionStart, el.selectionEnd);
                          if (cut) {
                              el.setRangeText("");
                          }
                      } else {
                          data = el.value;
                      }
                  } else {
                      // Even if CEF doesn't have focus, a disabled input could have a selection
                      var selection = window.document.getSelection();
                      if (selection.type === "Range") {
                          data = selection.toString();
                      }
                  }
  
                  if (typeof data === "string") {
                      var cutCopyEvent = new window.Event(cut ? "cut" : "copy", { bubbles: true });
                      el.dispatchEvent(cutCopyEvent);
  
                      return os.clipboardWrite(data);
                  }
  
                  // If we're on modal state (type edit), we should go with native copy/cut
                  if (this.flux.store("tool").getModalToolState()) {
                      if (cut) {
                          return this.transfer(nativeCut);
                      } else {
                          return this.transfer(nativeCopy);
                      }
                  } else if (!cut) {
                      var applicationStore = this.flux.store("application"),
                          document = applicationStore.getCurrentDocument();
  
                      if (!document || document.unsupported) {
                          return;
                      }
  
                      var layerIDs = collection.pluck(document.layers.selectedNormalized, "id"),
                          payload = {
                              document: document.id,
                              layers: layerIDs
                          },
                          rawPayload = JSON.stringify(payload);
  
                      headlights.logEvent("edit", "layers", "copy-layers");
                      return os.clipboardWrite(rawPayload, LAYER_CLIPBOARD_FORMAT);
                  }
              });
      };
  
      /**
       * Execute a cut operation on the currently active HTML element.
       *
       * @private
       * @return {Promise}
       */
      var cut = function () {
          return _cutOrCopy.call(this, true);
      };
      cut.action = {
          modal: true,
          reads: [locks.JS_TOOL, locks.PS_APP],
          writes: [locks.JS_DOC, locks.PS_DOC, locks.OS_CLIPBOARD],
          transfers: [nativeCut]
      };
  
      /**
       * Execute a copy operation on the currently active HTML element.
       *
       * @private
       * @return {Promise}
       */
      var copy = function () {
          return _cutOrCopy.call(this, false);
      };
      copy.action = {
          modal: true,
          reads: [locks.JS_DOC, locks.JS_TOOL, locks.PS_APP],
          writes: [locks.OS_CLIPBOARD],
          transfers: [nativeCopy]
      };
  
      /**
       * Execute a paste operation on the currently active HTML element.
       *
       * @private
       * @return {Promise}
       */
      var paste = function () {
          return os.hasKeyboardFocus()
              .bind(this)
              .then(function (cefHasFocus) {
                  var el = window.document.activeElement;
                  if (cefHasFocus && dom.isInput(el)) {
                      return os.clipboardRead()
                          .then(function (result) {
                              var data = result.data,
                                  format = result.format;
  
                              if (format !== "string") {
                                  return;
                              }
  
                              if (dom.isTextInput(el)) {
                                  var selectionStart = el.selectionStart;
                                  el.setRangeText(data);
                                  el.setSelectionRange(selectionStart + data.length, selectionStart + data.length);
                              } else {
                                  el.value = data;
                              }
  
                              var pasteEvent = new window.Event("paste", { bubbles: true });
                              el.dispatchEvent(pasteEvent);
                          });
                  } else {
                      return os.clipboardRead([LAYER_CLIPBOARD_FORMAT])
                          .bind(this)
                          .then(function (result) {
                              var format = result.format;
                              if (format !== LAYER_CLIPBOARD_FORMAT) {
                                  return this.transfer(nativePaste);
                              }
  
                              var applicationStore = this.flux.store("application"),
                                  document = applicationStore.getCurrentDocument();
  
                              if (!document || document.unsupported) {
                                  return;
                              }
  
                              var data = result.data,
                                  payload = JSON.parse(data),
                                  documentID = payload.document,
                                  documentStore = this.flux.store("document"),
                                  fromDocument = documentStore.getDocument(documentID);
  
                              if (!fromDocument || fromDocument.unsupported) {
                                  return;
                              }
  
                              var layerIDs = payload.layers,
                                  fromLayers = Immutable.List(layerIDs.reduce(function (layers, layerID) {
                                      var layer = fromDocument.layers.byID(layerID);
                                      if (layer) {
                                          layers.push(layer);
                                      }
                                      return layers;
                                  }, []));
  
                              headlights.logEvent("edit", "layers", "paste-layers");
                              return this.transfer(layers.duplicate, document, fromDocument, fromLayers);
                          });
                  }
              });
      };
      paste.action = {
          modal: true,
          reads: [locks.JS_DOC, locks.JS_APP, locks.OS_CLIPBOARD, locks.PS_APP],
          writes: [],
          transfers: [layers.duplicate, nativePaste]
      };
      /**
       * Execute a select operation on the currently active HTML element.
       *
       * @private
       * @return {Promise}
       */
      var selectAll = function () {
          return os.hasKeyboardFocus()
              .bind(this)
              .then(function (cefHasFocus) {
                  var el = window.document.activeElement;
                  if (cefHasFocus && dom.isInput(el)) {
                      if (dom.isTextInput(el)) {
                          el.setSelectionRange(0, el.value.length);
                      }
                  } else {
                      var toolStore = this.flux.store("tool");
                      if (toolStore.getModalToolState()) {
                          return this.transfer(nativeSelectAll);
                      } else {
                          return this.transfer(layers.selectAll);
                      }
                  }
              });
      };
      selectAll.action = {
          modal: true,
          reads: [locks.JS_TOOL, locks.PS_APP],
          writes: [],
          transfers: [layers.selectAll, nativeSelectAll]
      };
  
      /**
       * Step Backwards by transferring to the appropriate history action
       *
       * @private
       * @return {Promise}
       */
      var undo = function () {
          var currentDocument = this.flux.store("application").getCurrentDocument();
          if (!currentDocument) {
              return Promise.resolve();
          }
  
          return this.transfer(history.decrementHistory, currentDocument.id);
      };
      undo.action = {
          reads: [locks.JS_APP, locks.JS_DOC],
          writes: [],
          transfers: [history.decrementHistory],
          post: ["verify.layers.verifyLayerIndex"],
          modal: true,
          hideOverlays: true
      };
  
      /**
       * Step Forward by transferring to the appropriate history action
       *
       * @private
       * @return {Promise}
       */
      var redo = function () {
          var currentDocument = this.flux.store("application").getCurrentDocument();
          if (!currentDocument) {
              return Promise.resolve();
          }
  
          return this.transfer(history.incrementHistory, currentDocument.id);
      };
      redo.action = {
          reads: [locks.JS_APP, locks.JS_DOC],
          writes: [],
          transfers: [history.incrementHistory],
          post: ["verify.layers.verifyLayerIndex"],
          modal: true,
          hideOverlays: true
      };
  
      exports.nativeCut = nativeCut;
      exports.nativeCopy = nativeCopy;
      exports.nativePaste = nativePaste;
      exports.nativeSelectAll = nativeSelectAll;
      exports.cut = cut;
      exports.copy = copy;
      exports.paste = paste;
      exports.selectAll = selectAll;
      exports.undo = undo;
      exports.redo = redo;
  });
  