// # UI Modal
// A modal displays content that temporarily blocks interactions with a web site

;(function ( $, window, document, undefined ) {

$.fn.modal = function(parameters) {
  var
    // ## Group
    // Store a cached version of all elements initialized together
    $allModules = $(this),
    // Store cached versions of elements which are the same across all instances
    $window     = $(window),
    $document   = $(document),

    // Save a reference to the selector used for logs
    moduleSelector  = $allModules.selector || '',

    // Store a reference to current time for performance
    time            = new Date().getTime(),
    performance     = [],

    // Save a reference to arguments to access this 'special variable' outside of scope
    query           = arguments[0],
    methodInvoked   = (typeof query == 'string'),
    queryArguments  = [].slice.call(arguments, 1),

    returnedValue
  ;

  // ## Singular
  // Iterate over all elements to initialize module
  $allModules
    .each(function() {
      var
        // Extend setting if parameters is a settings object
        settings    = ( $.isPlainObject(parameters) )
          ? $.extend(true, {}, $.fn.modal.settings, parameters)
          : $.extend({}, $.fn.modal.settings),

        // Shortcut values for common settings
        selector        = settings.selector,
        className       = settings.className,
        namespace       = settings.namespace,
        error           = settings.error,

        // Namespace to store DOM Events
        eventNamespace  = '.' + namespace,

        // Namespace to store instance in metadata
        moduleNamespace = 'module-' + namespace,

        // Cache DOM elements
        $module      = $(this),
        $context     = $(settings.context),
        $otherModals = $allModules.not($module),
        $close       = $module.find(selector.close),

        // Set up blank variables for data stored across an instance
        $focusedElement,
        $dimmable,
        $dimmer,

        // Save a reference to the 'pure' DOM element node and current defined instance
        element      = this,
        instance     = $module.data(moduleNamespace),
        module
      ;

      // ## Module Behavior
      module  = {

        // ### Initialize
        // Attaches modal events to page
        initialize: function() {

          // Debug/Verbose allows for a trace to be passed for the javascript console
          module.verbose('Initializing dimmer', $context);

          // Make sure all dependencies are available or provide an error
          if(typeof $.fn.dimmer === undefined) {
            module.error(error.dimmer);
            return;
          }

          // Initialize a dimmer in the current modal context that
          // Dimmer appears and hides slightly quicker than modal to avoid race conditions in callbacks
          $dimmable = $context
            .dimmer({
              closable : false,
              show     : settings.duration * 0.95,
              hide     : settings.duration * 1.05
            })
            .dimmer('add content', $module)
          ;

          // Use Dimmer's Behavior API to retrieve a reference to the dimmer DOM element
          $dimmer = $dimmable
            .dimmer('get dimmer')
          ;

          module.verbose('Attaching close events', $close);

          // Attach some dimmer event
          $close
            .on('click' + eventNamespace, module.event.close)
          ;
          $window
            .on('resize', function() {
              module.event.debounce(module.refresh, 50);
            })
          ;
          module.instantiate();
        },

        // Store instance in metadata so it can be retrieved and modified on future invocations
        instantiate: function() {
          module.verbose('Storing instance of modal');
          // Immediately define possibly undefined instance
          instance = module;
          // Store new reference in metadata
          $module
            .data(moduleNamespace, instance)
          ;
        },

        // ### Destroy
        // Remove all module data from metadata and remove all events
        destroy: function() {
          module.verbose('Destroying previous modal');
          $module
            .removeData(moduleNamespace)
            .off(eventNamespace)
          ;
          // Child elements must also have their events removed
          $close
            .off(eventNamespace)
          ;
          // Destroy the initialized dimmer
          $context
            .dimmer('destroy')
          ;
        },

        // ### Refresh
        // Modal must modify its behavior depending on whether or not it can fit
        refresh: function() {
          // Remove scrolling/fixed type
          module.remove.scrolling();
          // Cache new module size
          module.cacheSizes();
          // Set type to either scrolling or fixed
          module.set.type();
          module.set.position();
        },

        // ### Attach events
        // Attaches any module method to another element selector
        attachEvents: function(selector, event) {
          var
            $toggle = $(selector)
          ;
          // default to toggle event if none specified
          event = $.isFunction(module[event])
            ? module[event]
            : module.toggle
          ;
          // determine whether element exists
          if($toggle.size() > 0) {
            module.debug('Attaching modal events to element', selector, event);
            // attach events
            $toggle
              .off(eventNamespace)
              .on('click' + eventNamespace, event)
            ;
          }
          else {
            module.error(error.notFound);
          }
        },

        // ### Events
        // Events contain event handlers where the ``this`` context may be specific to a part of an element
        event: {
          // ### Event Close
          // Make sure appropriate callback occurs before hiding dimmer on close
          close: function() {
            module.verbose('Closing element pressed');
            // Only hide modal if onDeny or onApprove return true
            if( $(this).is(selector.approve) ) {
              if($.proxy(settings.onApprove, element)()) {
                modal.hide();
              }
            }
            if( $(this).is(selector.deny) ) {
              if($.proxy(settings.onDeny, element)()) {
                modal.hide();
              }
            }
            else {
              module.hide();
            }
          },
          // ### Event Click
          // Only allow click event on dimmer to close modal if the click was not inside the dimmer
          click: function(event) {
            module.verbose('Determining if event occured on dimmer', event);
            if( $dimmer.find(event.target).size() === 0 ) {
              module.hide();
              event.stopImmediatePropagation();
            }
          },
          // ### Debounce
          // Make sure resize event fires maximum of once every 50ms
          debounce: function(method, delay) {
            clearTimeout(module.timer);
            module.timer = setTimeout(method, delay);
          },
          // ### Event Keyboard
          // Determine whether keydown event was on escape key
          keyboard: function(event) {
            var
              keyCode   = event.which,
              escapeKey = 27
            ;
            if(keyCode == escapeKey) {
              if(settings.closable) {
                module.debug('Escape key pressed hiding modal');
                module.hide();
              }
              else {
                module.debug('Escape key pressed, but closable is set to false');
              }
              event.preventDefault();
            }
          },
          // ### Event Resize
          // Only refresh if dimmer is visible
          resize: function() {
            if( $dimmable.dimmer('is active') ) {
              module.refresh();
            }
          }
        },

        // ### Toggle
        // Change visibility depending on current visibility state
        toggle: function() {
          if( module.is.active() ) {
            module.hide();
          }
          else {
            module.show();
          }
        },

        // ### Show
        // Find position then shows dimmer
        show: function() {
          module.showDimmer();
          module.cacheSizes();
          module.set.position();
          module.hideAll();
          // #### Loose Coupling
          if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
            // Use dimmer plugin if available
            $module
              .transition(settings.transition + ' in', settings.duration, module.set.active)
            ;
          }
          else {
            // Otherwise use fallback javascript animation
            $module
              .fadeIn(settings.duration, settings.easing, module.set.active)
            ;
          }
          module.debug('Triggering dimmer');
          $.proxy(settings.onShow, element)();
        },

        // ### Show Dimmer
        // Keep as a separate method to allow programmatic access via
        //    $('.modal').modal('show dimmer');
        showDimmer: function() {
          module.debug('Showing modal');
          $dimmable.dimmer('show');
        },

        // ### Hide
        // Determine whether to hide dimmer, modal, or both
        hide: function() {
          // Only attach close events if modal is allowed to close
          if(settings.closable) {
            $dimmer
              .off('click' + eventNamespace)
            ;
          }
          // Only hide dimmer once
          if( $dimmable.dimmer('is active') ) {
            $dimmable.dimmer('hide');
          }
          // Only hide modal once
          if( module.is.active() ) {
            module.hideModal();
            $.proxy(settings.onHide, element)();
          }
          else {
            module.debug('Cannot hide modal, modal is not visible');
          }
        },

        // ### Hide Dimmer
        // Hide dimmer from page
        hideDimmer: function() {
          module.debug('Hiding dimmer');
          $dimmable.dimmer('hide');
        },

        // ### Hide Modal
        // Hide modal from page
        hideModal: function() {
          module.debug('Hiding modal');
          module.remove.keyboardShortcuts();
          if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
            $module
              .transition(settings.transition + ' out', settings.duration, function() {
                module.remove.active();
                module.restore.focus();
              })
            ;
          }
          else {
            $module
              .fadeOut(settings.duration, settings.easing, function() {
                module.remove.active();
                module.restore.focus();
              })
            ;
          }
        },

        // ### Hide All
        // Make sure all other modals are hidden before showing a new one
        hideAll: function() {
          $otherModals
            .filter(':visible')
            .modal('hide')
          ;
        },

        // ### Add Keyboard Shorcuts
        // Add keyboard shortcut events to page
        add: {
          keyboardShortcuts: function() {
            module.verbose('Adding keyboard shortcuts');
            $document
              .on('keyup' + eventNamespace, module.event.keyboard)
            ;
          }
        },

        save: {
          // ### Save Focus
          // Save current focused element on modal show
          focus: function() {
            $focusedElement = $(document.activeElement).blur();
          }
        },

        restore: {
          // ### Restore Focus
          // Restore focus to previous element after modal is closed
          focus: function() {
            if($focusedElement && $focusedElement.size() > 0) {
              $focusedElement.focus();
            }
          }
        },

        // ### Remove
        // Utilities for removing and adding classes related to state
        remove: {
          active: function() {
            $module.removeClass(className.active);
          },
          keyboardShortcuts: function() {
            module.verbose('Removing keyboard shortcuts');
            $document
              .off('keyup' + eventNamespace)
            ;
          },
          scrolling: function() {
            $dimmable.removeClass(className.scrolling);
            $module.removeClass(className.scrolling);
          }
        },

        // ### Cache Sizes
        // Cache context and modal size to avoid recalculation
        cacheSizes: function() {
          module.cache = {
            height        : $module.outerHeight() + settings.offset,
            contextHeight : (settings.context == 'body')
              ? $(window).height()
              : $dimmable.height()
          };
          module.debug('Caching modal and container sizes', module.cache);
        },

        // ### Can Fit
        // Determine whether modal's cached size is smaller than context
        can: {
          fit: function() {
            return (module.cache.height < module.cache.contextHeight);
          }
        },

        // ### Is Active
        // Whether current element is active
        is: {
          active: function() {
            return $module.hasClass(className.active);
          }
        },

        set: {
          // ### Set Active
          active: function() {
            // Add escape key to close
            module.add.keyboardShortcuts();
            // Save reference to current focused element
            module.save.focus();
            // Set to scrolling or fixed
            module.set.type();
            // Add active class
            $module
              .addClass(className.active)
            ;
            // Add close events
            if(settings.closable) {
              $dimmer
                .on('click' + eventNamespace, module.event.click)
              ;
            }
          },
          // ### Set Scrolling
          // Add scrolling classes
          scrolling: function() {
            $dimmable.addClass(className.scrolling);
            $module.addClass(className.scrolling);
          },
          // ### Set Type
          // Set type to fixed or scrolling
          type: function() {
            if(module.can.fit()) {
              module.verbose('Modal fits on screen');
              module.remove.scrolling();
            }
            else {
              module.verbose('Modal cannot fit on screen setting to scrolling');
              module.set.scrolling();
            }
          },
          // ### Set Position
          // Position modal in center of page
          position: function() {
            module.verbose('Centering modal on page', module.cache, module.cache.height / 2);
            if(module.can.fit()) {
              // If fixed position center vertically
              $module
                .css({
                  top: '',
                  marginTop: -(module.cache.height / 2)
                })
              ;
            }
            else {
              // If modal is not fixed position place element 1em below current scrolled position in page
              $module
                .css({
                  marginTop : '1em',
                  top       : $document.scrollTop()
                })
              ;
            }
          }
        },

        // ### Settings
        // Used to modify or read setting(s) after initializing
        setting: function(name, value) {
          if( $.isPlainObject(name) ) {
            $.extend(true, settings, name);
          }
          else if(value !== undefined) {
            settings[name] = value;
          }
          else {
            return settings[name];
          }
        },
        // ### Internal
        // Used to modify or read methods from module
        internal: function(name, value) {
          if( $.isPlainObject(name) ) {
            $.extend(true, module, name);
          }
          else if(value !== undefined) {
            module[name] = value;
          }
          else {
            return module[name];
          }
        },


        // ### Debug
        // Debug pushes arguments to the console formatted as a debug statement
        debug: function() {
          if(settings.debug) {
            if(settings.performance) {
              module.performance.log(arguments);
            }
            else {
              module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
              module.debug.apply(console, arguments);
            }
          }
        },

        // ### Verbose
        // Calling verbose internally allows for additional data to be logged which can assist in debugging
        verbose: function() {
          if(settings.verbose && settings.debug) {
            if(settings.performance) {
              module.performance.log(arguments);
            }
            else {
              module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
              module.verbose.apply(console, arguments);
            }
          }
        },

        // ### Error
        // Error allows for the module to report named error messages, it may be useful to modify this to push error messages to the user. Error messages are defined in the modules settings object.
        error: function() {
          module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
          module.error.apply(console, arguments);
        },

        // ### Performance
        // This is called on each debug statement and logs the time since the last debug statement.
        performance: {
          log: function(message) {
            var
              currentTime,
              executionTime,
              previousTime
            ;
            if(settings.performance) {
              currentTime   = new Date().getTime();
              previousTime  = time || currentTime;
              executionTime = currentTime - previousTime;
              time          = currentTime;
              performance.push({
                'Element'        : element,
                'Name'           : message[0],
                'Arguments'      : [].slice.call(message, 1) || '',
                'Execution Time' : executionTime
              });
            }
            clearTimeout(module.performance.timer);
            module.performance.timer = setTimeout(module.performance.display, 100);
          },
          display: function() {
            var
              title = settings.name + ':',
              totalTime = 0
            ;
            time = false;
            clearTimeout(module.performance.timer);
            $.each(performance, function(index, data) {
              totalTime += data['Execution Time'];
            });
            title += ' ' + totalTime + 'ms';
            if(moduleSelector) {
              title += ' \'' + moduleSelector + '\'';
            }
            if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
              console.groupCollapsed(title);
              if(console.table) {
                console.table(performance);
              }
              else {
                $.each(performance, function(index, data) {
                  console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
                });
              }
              console.groupEnd();
            }
            performance = [];
          }
        },

        // ### Invoke
        // Invoke is used to match internal functions to string lookups.
        // `$('.foo').example('invoke', 'set text', 'Foo')`
        // Method lookups are lazy, looking for many variations of a search string
        // For example 'set active', will look for both `setText : function(){}`, `set: { text: function(){} }`
        // Invoke attempts to preserve the 'this' chaining unless a value is returned.
        // If multiple values are returned an array of values matching up to the length of the selector is returned
        invoke: function(query, passedArguments, context) {
          var
            maxDepth,
            found,
            response
          ;
          passedArguments = passedArguments || queryArguments;
          context         = element         || context;
          if(typeof query == 'string' && instance !== undefined) {
            query    = query.split(/[\. ]/);
            maxDepth = query.length - 1;
            $.each(query, function(depth, value) {
              var camelCaseValue = (depth != maxDepth)
                ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
                : query
              ;
              if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
                instance = instance[value];
              }
              else if( $.isPlainObject( instance[camelCaseValue] ) && (depth != maxDepth) ) {
                instance = instance[camelCaseValue];
              }
              else if( instance[value] !== undefined ) {
                found = instance[value];
                return false;
              }
              else if( instance[camelCaseValue] !== undefined ) {
                found = instance[camelCaseValue];
                return false;
              }
              else {
                module.error(error.method, query);
                return false;
              }
            });
          }
          if ( $.isFunction( found ) ) {
            response = found.apply(context, passedArguments);
          }
          else if(found !== undefined) {
            response = found;
          }
          // ### Invocation response
          // If a user passes in multiple elements invoke will be called for each element and the value will be returned in an array
          // For example ``$('.things').example('has text')`` with two elements might return ``[true, false]`` and for one element ``true``
          if($.isArray(returnedValue)) {
            returnedValue.push(response);
          }
          else if(returnedValue !== undefined) {
            returnedValue = [returnedValue, response];
          }
          else if(response !== undefined) {
            returnedValue = response;
          }
          return found;
        }
      };

      // ## Method Invocation

      // This is where the actual action occurs.
      //     $('.foo').module('set text', 'Ho hum');
      // If you call a module with a string parameter you are most likely trying to invoke a function
      if(methodInvoked) {
        if(instance === undefined) {
          module.initialize();
        }
        module.invoke(query);
      }
      // if no method call is required we simply initialize the plugin, destroying it if it exists already
      else {
        if(instance !== undefined) {
          module.destroy();
        }
        module.initialize();
      }
    })
  ;

  // ### Chaining
  // Return either jQuery chain or the returned value from invoke
  return (returnedValue !== undefined)
    ? returnedValue
    : this
  ;

};

// ## Settings
// These are the default configuration settings for any element initialized without parameters
$.fn.modal.settings = {

  // Name for debug logs
  name        : 'Modal',
  // Namespace for events and metadata
  namespace   : 'modal',

  // Whether to include all logging
  verbose     : true,
  // Whether to include important logs
  debug       : true,
  // Whether to show performance traces
  performance : true,

  // Whether modal is able to be closed by a user
  closable    : true,

  // Context which modal will be centered in
  context     : 'body',

  // Animation duration
  duration    : 500,
  // Animation easing
  easing      : 'easeOutExpo',

  // Vertical offset of modal
  offset      : 0,

  // Transition to use for animation
  transition  : 'scale',

  // Callback when modal shows
  onShow      : function(){},
  // Callback when modal hides
  onHide      : function(){},
  // Callback when modal approve action is called
  onApprove   : function(){ return true },
  // Callback when modal deny action is called
  onDeny      : function(){ return true },

  // List of selectors used to match behavior to DOM elements
  selector    : {
    close    : '.close, .actions .button',
    approve  : '.actions .positive, .actions .approve',
    deny     : '.actions .negative, .actions .cancel'
  },

  // List of error messages displayable to console
  error : {
    dimmer : 'UI Dimmer, a required component is not included in this page',
    method : 'The method you called is not defined.'
  },

  // List of class names used to represent element state
  className : {
    active    : 'active',
    scrolling : 'scrolling'
  },
};


})( jQuery, window , document );