//  angularModalService.js
//
//  Service for showing modal dialogs.

/***** JSLint Config *****/
/*global angular  */
(function() {

  'use strict';

  var module = angular.module('angularModalService', []);

  module.factory('ModalService', ['$animate', '$document', '$compile', '$controller', '$http', '$rootScope', '$q', '$templateRequest', '$timeout',
    function($animate, $document, $compile, $controller, $http, $rootScope, $q, $templateRequest, $timeout) {

    //  Get the body of the document, we'll add the modal to this.
    var body = angular.element($document[0].body);

    function ModalService() {

      var self = this;

      //  Returns a promise which gets the template, either
      //  from the template parameter or via a request to the
      //  template url parameter.
      var getTemplate = function(template, templateUrl) {
        var deferred = $q.defer();
        if (template) {
          deferred.resolve(template);
        } else if (templateUrl) {
          $templateRequest(templateUrl, true)
            .then(function(template) {
              deferred.resolve(template);
            }, function(error) {
              deferred.reject(error);
            });
        } else {
          deferred.reject("No template or templateUrl has been specified.");
        }
        return deferred.promise;
      };

      //  Adds an element to the DOM as the last child of its container
      //  like append, but uses $animate to handle animations. Returns a
      //  promise that is resolved once all animation is complete.
      var appendChild = function(parent, child) {
        var children = parent.children();
        if (children.length > 0) {
          return $animate.enter(child, parent, children[children.length - 1]);
        }
        return $animate.enter(child, parent);
      };

      self.showModal = function(options) {

        //  Create a deferred we'll resolve when the modal is ready.
        var deferred = $q.defer();

        //  Validate the input parameters.
        var controllerName = options.controller;
        if (!controllerName) {
          deferred.reject("No controller has been specified.");
          return deferred.promise;
        }

        //  Get the actual html of the template.
        getTemplate(options.template, options.templateUrl)
          .then(function(template) {

            //  Create a new scope for the modal.
            var modalScope = (options.scope || $rootScope).$new();

            //  Create the inputs object to the controller - this will include
            //  the scope, as well as all inputs provided.
            //  We will also create a deferred that is resolved with a provided
            //  close function. The controller can then call 'close(result)'.
            //  The controller can also provide a delay for closing - this is
            //  helpful if there are closing animations which must finish first.
            var closeDeferred = $q.defer();
            var closedDeferred = $q.defer();
            var inputs = {
              $scope: modalScope,
              close: function(result, delay) {
                if (delay === undefined || delay === null) delay = 0;
                $timeout(function() {
                  //  Resolve the 'close' promise.
                  closeDeferred.resolve(result);

                  //  Let angular remove the element and wait for animations to finish.
                  $animate.leave(modalElement)
                    .then(function () {
                      //  Resolve the 'closed' promise.
                      closedDeferred.resolve(result);

                      //  We can now clean up the scope
                      modalScope.$destroy();

                      //  Unless we null out all of these objects we seem to suffer
                      //  from memory leaks, if anyone can explain why then I'd
                      //  be very interested to know.
                      inputs.close = null;
                      deferred = null;
                      closeDeferred = null;
                      modal = null;
                      inputs = null;
                      modalElement = null;
                      modalScope = null;
                    });
                }, delay);
              }
            };

            //  If we have provided any inputs, pass them to the controller.
            if (options.inputs) angular.extend(inputs, options.inputs);

            //  Compile then link the template element, building the actual element.
            //  Set the $element on the inputs so that it can be injected if required.
            var linkFn = $compile(template);
            var modalElement = linkFn(modalScope);
            inputs.$element = modalElement;

            //  Create the controller, explicitly specifying the scope to use.
            var controllerObjBefore = modalScope[options.controllerAs];
            var modalController = $controller(options.controller, inputs, false, options.controllerAs);

            if (options.controllerAs && controllerObjBefore) {
              angular.extend(modalController, controllerObjBefore);
            }

            //  Finally, append the modal to the dom.
            if (options.appendElement) {
              // append to custom append element
              appendChild(options.appendElement, modalElement);
            } else {
              // append to body when no custom append element is specified
              appendChild(body, modalElement);
            }

            //  We now have a modal object...
            var modal = {
              controller: modalController,
              scope: modalScope,
              element: modalElement,
              close: closeDeferred.promise,
              closed: closedDeferred.promise
            };

            //  ...which is passed to the caller via the promise.
            deferred.resolve(modal);

          })
          .then(null, function(error) { // 'catch' doesn't work in IE8.
            deferred.reject(error);
          });

        return deferred.promise;
      };

    }

    return new ModalService();
  }]);

}());
