text: ﻿/**
   * The activator module encapsulates all logic related to screen/component activation.
   * An activator is essentially an asynchronous state machine that understands a particular state transition protocol.
   * The protocol ensures that the following series of events always occur: `canDeactivate` (previous state), `canActivate` (new state), `deactivate` (previous state), `activate` (new state).
   * Each of the _can_ callbacks may return a boolean, affirmative value or promise for one of those. If either of the _can_ functions yields a false result, then activation halts.
   * @module activator
   * @requires system
   * @requires knockout
   */
  define(['durandal/system', 'knockout'], function (system, ko) {
      var activator;
      var defaultOptions = {
          canDeactivate:true
      };
  
      function ensureSettings(settings) {
          if (settings == undefined) {
              settings = {};
          }
  
          if (!system.isBoolean(settings.closeOnDeactivate)) {
              settings.closeOnDeactivate = activator.defaults.closeOnDeactivate;
          }
  
          if (!settings.beforeActivate) {
              settings.beforeActivate = activator.defaults.beforeActivate;
          }
  
          if (!settings.afterDeactivate) {
              settings.afterDeactivate = activator.defaults.afterDeactivate;
          }
  
          if(!settings.affirmations){
              settings.affirmations = activator.defaults.affirmations;
          }
  
          if (!settings.interpretResponse) {
              settings.interpretResponse = activator.defaults.interpretResponse;
          }
  
          if (!settings.areSameItem) {
              settings.areSameItem = activator.defaults.areSameItem;
          }
  
          if (!settings.findChildActivator) {
              settings.findChildActivator = activator.defaults.findChildActivator;
          }
  
          return settings;
      }
  
      function invoke(target, method, data) {
          if (system.isArray(data)) {
              return target[method].apply(target, data);
          }
  
          return target[method](data);
      }
  
      function deactivate(item, close, settings, dfd, setter) {
          if (item && item.deactivate) {
              system.log('Deactivating', item);
  
              var result;
              try {
                  result = item.deactivate(close);
              } catch(error) {
                  system.log('ERROR: ' + error.message, error);
                  dfd.resolve(false);
                  return;
              }
  
              if (result && result.then) {
                  result.then(function() {
                      settings.afterDeactivate(item, close, setter);
                      dfd.resolve(true);
                  }, function(reason) {
                      if (reason) {
                          system.log(reason);
                      }
  
                      dfd.resolve(false);
                  });
              } else {
                  settings.afterDeactivate(item, close, setter);
                  dfd.resolve(true);
              }
          } else {
              if (item) {
                  settings.afterDeactivate(item, close, setter);
              }
  
              dfd.resolve(true);
          }
      }
  
      function activate(newItem, activeItem, callback, activationData) {
          var result;
  
          if(newItem && newItem.activate) {
              system.log('Activating', newItem);
  
              try {
                  result = invoke(newItem, 'activate', activationData);
              } catch (error) {
                  system.log('ERROR: ' + error.message, error);
                  callback(false);
                  return;
              }
          }
  
          if(result && result.then) {
              result.then(function() {
                  activeItem(newItem);
                  callback(true);
              }, function (reason) {
                  if (reason) {
                      system.log('ERROR: ' + reason.message, reason);
                  }
  
                  callback(false);
              });
          } else {
              activeItem(newItem);
              callback(true);
          }
      }
  
      function canDeactivateItem(item, close, settings, options) {
          options = system.extend({}, defaultOptions, options);
          settings.lifecycleData = null;
  
          return system.defer(function (dfd) {
              function continueCanDeactivate() {
                  if (item && item.canDeactivate && options.canDeactivate) {
                      var resultOrPromise;
                      try {
                          resultOrPromise = item.canDeactivate(close);
                      } catch (error) {
                          system.log('ERROR: ' + error.message, error);
                          dfd.resolve(false);
                          return;
                      }
  
                      if (resultOrPromise.then) {
                          resultOrPromise.then(function (result) {
                              settings.lifecycleData = result;
                              dfd.resolve(settings.interpretResponse(result));
                          }, function (reason) {
                              if (reason) {
                                  system.log('ERROR: ' + reason.message, reason);
                              }
  
                              dfd.resolve(false);
                          });
                      } else {
                          settings.lifecycleData = resultOrPromise;
                          dfd.resolve(settings.interpretResponse(resultOrPromise));
                      }
                  } else {
                      dfd.resolve(true);
                  }
              }
  
              var childActivator = settings.findChildActivator(item);
              if (childActivator) {
                  childActivator.canDeactivate().then(function(result) {
                      if (result) {
                          continueCanDeactivate();
                      } else {
                          dfd.resolve(false);
                      }
                  });
              } else {
                  continueCanDeactivate();
              }
          }).promise();
      };
  
      function canActivateItem(newItem, activeItem, settings, activeData, newActivationData) {
          settings.lifecycleData = null;
  
          return system.defer(function (dfd) {
              if (settings.areSameItem(activeItem(), newItem, activeData, newActivationData)) {
                  dfd.resolve(true);
                  return;
              }
  
              if (newItem && newItem.canActivate) {
                  var resultOrPromise;
                  try {
                      resultOrPromise = invoke(newItem, 'canActivate', newActivationData);
                  } catch (error) {
                      system.log('ERROR: ' + error.message, error);
                      dfd.resolve(false);
                      return;
                  }
  
                  if (resultOrPromise.then) {
                      resultOrPromise.then(function(result) {
                          settings.lifecycleData = result;
                          dfd.resolve(settings.interpretResponse(result));
                      }, function(reason) {
                          if (reason) {
                              system.log('ERROR: ' + reason.message, reason);
                          }
  
                          dfd.resolve(false);
                      });
                  } else {
                      settings.lifecycleData = resultOrPromise;
                      dfd.resolve(settings.interpretResponse(resultOrPromise));
                  }
              } else {
                  dfd.resolve(true);
              }
          }).promise();
      };
  
      /**
       * An activator is a read/write computed observable that enforces the activation lifecycle whenever changing values.
       * @class Activator
       */
      function createActivator(initialActiveItem, settings) {
          var activeItem = ko.observable(null);
          var activeData;
  
          settings = ensureSettings(settings);
  
          var computed = ko.computed({
              read: function () {
                  return activeItem();
              },
              write: function (newValue) {
                  computed.viaSetter = true;
                  computed.activateItem(newValue);
              }
          });
  
          computed.__activator__ = true;
  
          /**
           * The settings for this activator.
           * @property {ActivatorSettings} settings
           */
          computed.settings = settings;
          settings.activator = computed;
  
          /**
           * An observable which indicates whether or not the activator is currently in the process of activating an instance.
           * @method isActivating
           * @return {boolean}
           */
          computed.isActivating = ko.observable(false);
  
          computed.forceActiveItem = function (item) {
              activeItem(item);
          };
  
          /**
           * Determines whether or not the specified item can be deactivated.
           * @method canDeactivateItem
           * @param {object} item The item to check.
           * @param {boolean} close Whether or not to check if close is possible.
           * @param {object} options Options for controlling the activation process.
           * @return {promise}
           */
          computed.canDeactivateItem = function (item, close, options) {
              return canDeactivateItem(item, close, settings, options);
          };
  
          /**
           * Deactivates the specified item.
           * @method deactivateItem
           * @param {object} item The item to deactivate.
           * @param {boolean} close Whether or not to close the item.
           * @return {promise}
           */
          computed.deactivateItem = function (item, close) {
              return system.defer(function(dfd) {
                  computed.canDeactivateItem(item, close).then(function(canDeactivate) {
                      if (canDeactivate) {
                          deactivate(item, close, settings, dfd, activeItem);
                      } else {
                          computed.notifySubscribers();
                          dfd.resolve(false);
                      }
                  });
              }).promise();
          };
  
          /**
           * Determines whether or not the specified item can be activated.
           * @method canActivateItem
           * @param {object} item The item to check.
           * @param {object} activationData Data associated with the activation.
           * @return {promise}
           */
          computed.canActivateItem = function (newItem, activationData) {
              return canActivateItem(newItem, activeItem, settings, activeData, activationData);
          };
  
          /**
           * Activates the specified item.
           * @method activateItem
           * @param {object} newItem The item to activate.
           * @param {object} newActivationData Data associated with the activation.
           * @param {object} options Options for controlling the activation process.
           * @return {promise}
           */
          computed.activateItem = function (newItem, newActivationData, options) {
              var viaSetter = computed.viaSetter;
              computed.viaSetter = false;
  
              return system.defer(function (dfd) {
                  if (computed.isActivating()) {
                      dfd.resolve(false);
                      return;
                  }
  
                  computed.isActivating(true);
  
                  var currentItem = activeItem();
                  if (settings.areSameItem(currentItem, newItem, activeData, newActivationData)) {
                      computed.isActivating(false);
                      dfd.resolve(true);
                      return;
                  }
  
                  computed.canDeactivateItem(currentItem, settings.closeOnDeactivate, options).then(function (canDeactivate) {
                      if (canDeactivate) {
                          computed.canActivateItem(newItem, newActivationData).then(function (canActivate) {
                              if (canActivate) {
                                  system.defer(function (dfd2) {
                                      deactivate(currentItem, settings.closeOnDeactivate, settings, dfd2);
                                  }).promise().then(function () {
                                          newItem = settings.beforeActivate(newItem, newActivationData);
                                          activate(newItem, activeItem, function (result) {
                                              activeData = newActivationData;
                                              computed.isActivating(false);
                                              dfd.resolve(result);
                                          }, newActivationData);
                                      });
                              } else {
                                  if (viaSetter) {
                                      computed.notifySubscribers();
                                  }
  
                                  computed.isActivating(false);
                                  dfd.resolve(false);
                              }
                          });
                      } else {
                          if (viaSetter) {
                              computed.notifySubscribers();
                          }
  
                          computed.isActivating(false);
                          dfd.resolve(false);
                      }
                  });
              }).promise();
          };
  
          /**
           * Determines whether or not the activator, in its current state, can be activated.
           * @method canActivate
           * @return {promise}
           */
          computed.canActivate = function () {
              var toCheck;
  
              if (initialActiveItem) {
                  toCheck = initialActiveItem;
                  initialActiveItem = false;
              } else {
                  toCheck = computed();
              }
  
              return computed.canActivateItem(toCheck);
          };
  
          /**
           * Activates the activator, in its current state.
           * @method activate
           * @return {promise}
           */
          computed.activate = function () {
              var toActivate;
  
              if (initialActiveItem) {
                  toActivate = initialActiveItem;
                  initialActiveItem = false;
              } else {
                  toActivate = computed();
              }
  
              return computed.activateItem(toActivate);
          };
  
          /**
           * Determines whether or not the activator, in its current state, can be deactivated.
           * @method canDeactivate
           * @return {promise}
           */
          computed.canDeactivate = function (close) {
              return computed.canDeactivateItem(computed(), close);
          };
  
          /**
           * Deactivates the activator, in its current state.
           * @method deactivate
           * @return {promise}
           */
          computed.deactivate = function (close) {
              return computed.deactivateItem(computed(), close);
          };
  
          computed.includeIn = function (includeIn) {
              includeIn.canActivate = function () {
                  return computed.canActivate();
              };
  
              includeIn.activate = function () {
                  return computed.activate();
              };
  
              includeIn.canDeactivate = function (close) {
                  return computed.canDeactivate(close);
              };
  
              includeIn.deactivate = function (close) {
                  return computed.deactivate(close);
              };
          };
  
          if (settings.includeIn) {
              computed.includeIn(settings.includeIn);
          } else if (initialActiveItem) {
              computed.activate();
          }
  
          computed.forItems = function (items) {
              settings.closeOnDeactivate = false;
  
              settings.determineNextItemToActivate = function (list, lastIndex) {
                  var toRemoveAt = lastIndex - 1;
  
                  if (toRemoveAt == -1 && list.length > 1) {
                      return list[1];
                  }
  
                  if (toRemoveAt > -1 && toRemoveAt < list.length - 1) {
                      return list[toRemoveAt];
                  }
  
                  return null;
              };
  
              settings.beforeActivate = function (newItem) {
                  var currentItem = computed();
  
                  if (!newItem) {
                      newItem = settings.determineNextItemToActivate(items, currentItem ? items.indexOf(currentItem) : 0);
                  } else {
                      var index = items.indexOf(newItem);
  
                      if (index == -1) {
                          items.push(newItem);
                      } else {
                          newItem = items()[index];
                      }
                  }
  
                  return newItem;
              };
  
              settings.afterDeactivate = function (oldItem, close) {
                  if (close) {
                      items.remove(oldItem);
                  }
              };
  
              var originalCanDeactivate = computed.canDeactivate;
              computed.canDeactivate = function (close) {
                  if (close) {
                      return system.defer(function (dfd) {
                          var list = items();
                          var results = [];
  
                          function finish() {
                              for (var j = 0; j < results.length; j++) {
                                  if (!results[j]) {
                                      dfd.resolve(false);
                                      return;
                                  }
                              }
  
                              dfd.resolve(true);
                          }
  
                          for (var i = 0; i < list.length; i++) {
                              computed.canDeactivateItem(list[i], close).then(function (result) {
                                  results.push(result);
                                  if (results.length == list.length) {
                                      finish();
                                  }
                              });
                          }
                      }).promise();
                  } else {
                      return originalCanDeactivate();
                  }
              };
  
              var originalDeactivate = computed.deactivate;
              computed.deactivate = function (close) {
                  if (close) {
                      return system.defer(function (dfd) {
                          var list = items();
                          var results = 0;
                          var listLength = list.length;
  
                          function doDeactivate(item) {
                              setTimeout(function () {
                                  computed.deactivateItem(item, close).then(function () {
                                      results++;
                                      items.remove(item);
                                      if (results == listLength) {
                                          dfd.resolve();
                                      }
                                  });
                              }, 1);
                          }
  
                          for (var i = 0; i < listLength; i++) {
                              doDeactivate(list[i]);
                          }
                      }).promise();
                  } else {
                      return originalDeactivate();
                  }
              };
  
              return computed;
          };
  
          return computed;
      }
  
      /**
       * @class ActivatorSettings
       * @static
       */
      var activatorSettings = {
          /**
           * The default value passed to an object's deactivate function as its close parameter.
           * @property {boolean} closeOnDeactivate
           * @default true
           */
          closeOnDeactivate: true,
          /**
           * Lower-cased words which represent a truthy value.
           * @property {string[]} affirmations
           * @default ['yes', 'ok', 'true']
           */
          affirmations: ['yes', 'ok', 'true'],
          /**
           * Interprets the response of a `canActivate` or `canDeactivate` call using the known affirmative values in the `affirmations` array.
           * @method interpretResponse
           * @param {object} value
           * @return {boolean}
           */
          interpretResponse: function(value) {
              if(system.isObject(value)) {
                  value = value.can || false;
              }
  
              if(system.isString(value)) {
                  return ko.utils.arrayIndexOf(this.affirmations, value.toLowerCase()) !== -1;
              }
  
              return value;
          },
          /**
           * Determines whether or not the current item and the new item are the same.
           * @method areSameItem
           * @param {object} currentItem
           * @param {object} newItem
           * @param {object} currentActivationData
           * @param {object} newActivationData
           * @return {boolean}
           */
          areSameItem: function(currentItem, newItem, currentActivationData, newActivationData) {
              return currentItem == newItem;
          },
          /**
           * Called immediately before the new item is activated.
           * @method beforeActivate
           * @param {object} newItem
           */
          beforeActivate: function(newItem) {
              return newItem;
          },
          /**
           * Called immediately after the old item is deactivated.
           * @method afterDeactivate
           * @param {object} oldItem The previous item.
           * @param {boolean} close Whether or not the previous item was closed.
           * @param {function} setter The activate item setter function.
           */
          afterDeactivate: function(oldItem, close, setter) {
              if(close && setter) {
                  setter(null);
              }
          },
          findChildActivator: function(item){
              return null;
          }
      };
  
      /**
       * @class ActivatorModule
       * @static
       */
      activator = {
          /**
           * The default settings used by activators.
           * @property {ActivatorSettings} defaults
           */
          defaults: activatorSettings,
          /**
           * Creates a new activator.
           * @method create
           * @param {object} [initialActiveItem] The item which should be immediately activated upon creation of the ativator.
           * @param {ActivatorSettings} [settings] Per activator overrides of the default activator settings.
           * @return {Activator} The created activator.
           */
          create: createActivator,
          /**
           * Determines whether or not the provided object is an activator or not.
           * @method isActivator
           * @param {object} object Any object you wish to verify as an activator or not.
           * @return {boolean} True if the object is an activator; false otherwise.
           */
          isActivator:function(object){
              return object && object.__activator__;
          }
      };
  
      return activator;
  });