/*

* # Semantic - API
* http://github.com/semantic-org/semantic-ui/
*
*
* Copyright 2014 Contributor
* Released under the MIT license
* http://opensource.org/licenses/MIT
*
*/

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

$.api = $.fn.api = function(parameters) {

var
  // use window context if none specified
  $allModules     = $.isFunction(this)
      ? $(window)
      : $(this),
  moduleSelector = $allModules.selector || '',
  time           = new Date().getTime(),
  performance    = [],

  query          = arguments[0],
  methodInvoked  = (typeof query == 'string'),
  queryArguments = [].slice.call(arguments, 1),

  returnedValue
;

$allModules
  .each(function() {
    var
      settings          = ( $.isPlainObject(parameters) )
        ? $.extend(true, {}, $.fn.api.settings, parameters)
        : $.extend({}, $.fn.api.settings),

      // internal aliases
      namespace       = settings.namespace,
      metadata        = settings.metadata,
      selector        = settings.selector,
      error           = settings.error,
      className       = settings.className,

      // define namespaces for modules
      eventNamespace  = '.' + namespace,
      moduleNamespace = 'module-' + namespace,

      // element that creates request
      $module         = $(this),
      $form           = $module.closest(selector.form),

      // context used for state
      $context        = (settings.stateContext)
        ? $(settings.stateContext)
        : $module,

      // request details
      ajaxSettings,
      requestSettings,
      url,
      data,

      // standard module
      element         = this,
      context         = $context.get(),
      instance        = $module.data(moduleNamespace),
      module
    ;

    module = {

      initialize: function() {
        var
          triggerEvent = module.get.event()
        ;
        // bind events
        if(!methodInvoked) {
          if( triggerEvent ) {
            module.debug('Attaching API events to element', triggerEvent);
            $module
              .on(triggerEvent + eventNamespace, module.event.trigger)
            ;
          }
          else {
            module.query();
          }
        }
        module.instantiate();
      },

      instantiate: function() {
        module.verbose('Storing instance of module', module);
        instance = module;
        $module
          .data(moduleNamespace, instance)
        ;
      },

      destroy: function() {
        module.verbose('Destroying previous module for', element);
        $module
          .removeData(moduleNamespace)
          .off(eventNamespace)
        ;
      },

      query: function() {

        if(module.is.disabled()) {
          module.debug('Element is disabled API request aborted');
          return;
        }
        // determine if an api event already occurred
        if(module.is.loading() && settings.throttle === 0 ) {
          module.debug('Cancelling request, previous request is still pending');
          return;
        }

        // pass element metadata to url (value, text)
        if(settings.defaultData) {
          $.extend(true, settings.urlData, module.get.defaultData());
        }

        // Add form content
        if(settings.serializeForm !== false || $context.is('form')) {
          if(settings.serializeForm == 'json') {
            $.extend(true, settings.data, module.get.formData());
          }
          else {
            settings.data = module.get.formData();
          }
        }

        // call beforesend and get any settings changes
        requestSettings = module.get.settings();

        // check if beforesend cancelled request
        if(requestSettings === false) {
          module.error(error.beforeSend);
          return;
        }

        if(settings.url) {
          // override with url if specified
          module.debug('Using specified url', url);
          url = module.add.urlData( settings.url );
        }
        else {
          // otherwise find url from api endpoints
          url = module.add.urlData( module.get.templateURL() );
          module.debug('Added URL Data to url', url);
        }

        // exit conditions reached, missing url parameters
        if( !url ) {
          if($module.is('form')) {
            module.debug('No url or action specified, defaulting to form action');
            url = $module.attr('action');
          }
          else {
            module.error(error.missingURL, settings.action);
            return;
          }
        }

        // add loading state
        module.set.loading();

        // look for jQuery ajax parameters in settings
        ajaxSettings = $.extend(true, {}, settings, {
          type       : settings.method || settings.type,
          data       : data,
          url        : settings.base + url,
          beforeSend : settings.beforeXHR,
          success    : function() {},
          failure    : function() {},
          complete   : function() {}
        });

        module.verbose('Creating AJAX request with settings', ajaxSettings);

        if( !module.is.loading() ) {
          module.request = module.create.request();
          module.xhr = module.create.xhr();
        }
        else {
          // throttle additional requests
          module.timer = setTimeout(function() {
            module.request = module.create.request();
            module.xhr = module.create.xhr();
          }, settings.throttle);
        }

      },

      is: {
        disabled: function() {
          return ($module.filter(settings.filter).size() > 0);
        },
        loading: function() {
          return (module.request && module.request.state() == 'pending');
        }
      },

      was: {
        succesful: function() {
          return (module.request && module.request.state() == 'resolved');
        },
        failure: function() {
          return (module.request && module.request.state() == 'rejected');
        },
        complete: function() {
          return (module.request && (module.request.state() == 'resolved' || module.request.state() == 'rejected') );
        }
      },

      add: {
        urlData: function(url, urlData) {
          var
            requiredVariables,
            optionalVariables
          ;
          if(url) {
            requiredVariables = url.match(settings.regExp.required);
            optionalVariables = url.match(settings.regExp.optional);
            urlData           = urlData || settings.urlData;
            if(requiredVariables) {
              module.debug('Looking for required URL variables', requiredVariables);
              $.each(requiredVariables, function(index, templatedString) {
                var
                  // allow legacy {$var} style
                  variable = (templatedString.indexOf('$') !== -1)
                    ? templatedString.substr(2, templatedString.length - 3)
                    : templatedString.substr(1, templatedString.length - 2),
                  value   = ($.isPlainObject(urlData) && urlData[variable] !== undefined)
                    ? urlData[variable]
                    : ($module.data(variable) !== undefined)
                      ? $module.data(variable)
                      : ($context.data(variable) !== undefined)
                        ? $context.data(variable)
                        : urlData[variable]
                ;
                // remove value
                if(value === undefined) {
                  module.error(error.requiredParameter, variable, url);
                  url = false;
                  return false;
                }
                else {
                  module.verbose('Found required variable', variable, value);
                  url = url.replace(templatedString, value);
                }
              });
            }
            if(optionalVariables) {
              module.debug('Looking for optional URL variables', requiredVariables);
              $.each(optionalVariables, function(index, templatedString) {
                var
                  // allow legacy {/$var} style
                  variable = (templatedString.indexOf('$') !== -1)
                    ? templatedString.substr(3, templatedString.length - 4)
                    : templatedString.substr(2, templatedString.length - 3),
                  value   = ($.isPlainObject(urlData) && urlData[variable] !== undefined)
                    ? urlData[variable]
                    : ($module.data(variable) !== undefined)
                      ? $module.data(variable)
                      : ($context.data(variable) !== undefined)
                        ? $context.data(variable)
                        : urlData[variable]
                ;
                // optional replacement
                if(value !== undefined) {
                  module.verbose('Optional variable Found', variable, value);
                  url = url.replace(templatedString, value);
                }
                else {
                  module.verbose('Optional variable not found', variable);
                  // remove preceding slash if set
                  if(url.indexOf('/' + templatedString) !== -1) {
                    url = url.replace('/' + templatedString, '');
                  }
                  else {
                    url = url.replace(templatedString, '');
                  }
                }
              });
            }
          }
          return url;
        }
      },

      event: {
        trigger: function(event) {
          module.query();
          if(event.type == 'submit' || event.type == 'click') {
            event.preventDefault();
          }
        },
        xhr: {
          always: function() {
            // calculate if loading time was below minimum threshold
          },
          done: function(response) {
            var
              context      = this,
              elapsedTime  = (new Date().getTime() - time),
              timeLeft     = (settings.loadingDuration - elapsedTime)
            ;
            timeLeft = (timeLeft > 0)
              ? timeLeft
              : 0
            ;
            setTimeout(function() {
              module.request.resolveWith(context, [response]);
            }, timeLeft);
          },
          fail: function(xhr, status, httpMessage) {
            var
              context     = this,
              elapsedTime = (new Date().getTime() - time),
              timeLeft    = (settings.loadingDuration - elapsedTime)
            ;
            timeLeft = (timeLeft > 0)
              ? timeLeft
              : 0
            ;
            // page triggers abort on navigation, dont show error
            setTimeout(function() {
              if(status !== 'abort') {
                module.request.rejectWith(context, [xhr, status, httpMessage]);
              }
              else {
                module.reset();
              }
            }, timeLeft);
          }
        },
        request: {
          complete: function(response) {
            module.remove.loading();
            $.proxy(settings.onComplete, context)(response, $module);
          },
          done: function(response) {
            module.debug('API Response Received', response);
            if(settings.dataType == 'json') {
              if( $.isFunction(settings.successTest) ) {
                module.debug('Checking JSON returned success', settings.successTest, response);
                if( settings.successTest(response) ) {
                  $.proxy(settings.onSuccess, context)(response, $module);
                }
                else {
                  module.debug('JSON test specified by user and response failed', response);
                  $.proxy(settings.onFailure, context)(response, $module);
                }
              }
              else {
                $.proxy(settings.onSuccess, context)(response, $module);
              }
            }
            else {
              $.proxy(settings.onSuccess, context)(response, $module);
            }
          },
          error: function(xhr, status, httpMessage) {
            var
              errorMessage = (settings.error[status] !== undefined)
                ? settings.error[status]
                : httpMessage,
              response
            ;
            // let em know unless request aborted
            if(xhr !== undefined) {
              // readyState 4 = done, anything less is not really sent
              if(xhr.readyState !== undefined && xhr.readyState == 4) {

                // if http status code returned and json returned error, look for it
                if( xhr.status != 200 && httpMessage !== undefined && httpMessage !== '') {
                  module.error(error.statusMessage + httpMessage);
                }
                else {
                  if(status == 'error' && settings.dataType == 'json') {
                    try {
                      response = $.parseJSON(xhr.responseText);
                      if(response && response.error !== undefined) {
                        errorMessage = response.error;
                      }
                    }
                    catch(e) {
                      module.error(error.JSONParse);
                    }
                  }
                }
                module.remove.loading();
                module.set.error();
                // show error state only for duration specified in settings
                if(settings.errorDuration) {
                  setTimeout(module.remove.error, settings.errorDuration);
                }
                module.debug('API Request error:', errorMessage);
                $.proxy(settings.onError, context)(errorMessage, context);
              }
              else {
                $.proxy(settings.onAbort, context)(errorMessage, context);
                module.debug('Request Aborted (Most likely caused by page change or CORS Policy)', status, httpMessage);
              }
            }
          }
        }
      },

      create: {
        request: function() {
          return $.Deferred()
            .always(module.event.request.complete)
            .done(module.event.request.done)
            .fail(module.event.request.error)
          ;
        },
        xhr: function() {
          $.ajax(ajaxSettings)
            .always(module.event.xhr.always)
            .done(module.event.xhr.done)
            .fail(module.event.xhr.fail)
          ;
        }
      },

      set: {
        error: function() {
          module.verbose('Adding error state to element', $context);
          $context.addClass(className.error);
        },
        loading: function() {
          module.verbose('Adding loading state to element', $context);
          $context.addClass(className.loading);
        }
      },

      remove: {
        error: function() {
          module.verbose('Removing error state from element', $context);
          $context.removeClass(className.error);
        },
        loading: function() {
          module.verbose('Removing loading state from element', $context);
          $context.removeClass(className.loading);
        }
      },

      get: {
        request: function() {
          return module.request || false;
        },
        xhr: function() {
          return module.xhr || false;
        },
        settings: function() {
          var
            runSettings
          ;
          runSettings = $.proxy(settings.beforeSend, $module)(settings);
          if(runSettings) {
            if(runSettings.success !== undefined) {
              module.debug('Legacy success callback detected', runSettings);
              module.error(error.legacyParameters, runSettings.success);
              runSettings.onSuccess = runSettings.success;
            }
            if(runSettings.failure !== undefined) {
              module.debug('Legacy failure callback detected', runSettings);
              module.error(error.legacyParameters, runSettings.failure);
              runSettings.onFailure = runSettings.failure;
            }
            if(runSettings.complete !== undefined) {
              module.debug('Legacy complete callback detected', runSettings);
              module.error(error.legacyParameters, runSettings.complete);
              runSettings.onComplete = runSettings.complete;
            }
          }
          if(runSettings === undefined) {
            module.error(error.noReturnedValue);
          }
          return (runSettings !== undefined)
            ? runSettings
            : settings
          ;
        },
        defaultData: function() {
          var
            data = {}
          ;
          if( !$.isWindow(element) ) {
            if( $module.is('input') ) {
              data.value = $module.val();
            }
            else if( $module.is('form') ) {

            }
            else {
              data.text = $module.text();
            }
          }
          return data;
        },
        event: function() {
          if( $.isWindow(element) || settings.on == 'now' ) {
            module.debug('API called without element, no events attached');
            return false;
          }
          else if(settings.on == 'auto') {
            if( $module.is('input') ) {
              return (element.oninput !== undefined)
                ? 'input'
                : (element.onpropertychange !== undefined)
                  ? 'propertychange'
                  : 'keyup'
              ;
            }
            else if( $module.is('form') ) {
              return 'submit';
            }
            else {
              return 'click';
            }
          }
          else {
            return settings.on;
          }
        },
        formData: function() {
          var
            formData
          ;
          if($(this).serializeObject() !== undefined) {
            formData = $form.serializeObject();
          }
          else {
            module.error(error.missingSerialize);
            formData = $form.serialize();
          }
          module.debug('Retrieved form data', formData);
          return formData;
        },
        templateURL: function(action) {
          var
            url
          ;
          action = action || $module.data(settings.metadata.action) || settings.action || false;
          if(action) {
            module.debug('Looking up url for action', action, settings.api);
            if(settings.api[action] !== undefined) {
              url = settings.api[action];
              module.debug('Found template url', url);
            }
            else {
              module.error(error.missingAction, settings.action, settings.api);
            }
          }
          return url;
        }
      },

      // reset state
      reset: function() {
        module.remove.error();
        module.remove.loading();
      },

      setting: function(name, value) {
        module.debug('Changing setting', name, value);
        if( $.isPlainObject(name) ) {
          $.extend(true, settings, name);
        }
        else if(value !== undefined) {
          settings[name] = value;
        }
        else {
          return settings[name];
        }
      },
      internal: function(name, value) {
        if( $.isPlainObject(name) ) {
          $.extend(true, module, name);
        }
        else if(value !== undefined) {
          module[name] = value;
        }
        else {
          return module[name];
        }
      },
      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: 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: function() {
        module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
        module.error.apply(console, arguments);
      },
      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({
              'Name'           : message[0],
              'Arguments'      : [].slice.call(message, 1) || '',
              //'Element'        : element,
              '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: function(query, passedArguments, context) {
        var
          object = instance,
          maxDepth,
          found,
          response
        ;
        passedArguments = passedArguments || queryArguments;
        context         = element         || context;
        if(typeof query == 'string' && object !== 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( object[camelCaseValue] ) && (depth != maxDepth) ) {
              object = object[camelCaseValue];
            }
            else if( object[camelCaseValue] !== undefined ) {
              found = object[camelCaseValue];
              return false;
            }
            else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
              object = object[value];
            }
            else if( object[value] !== undefined ) {
              found = object[value];
              return false;
            }
            else {
              module.error(error.method, query);
              return false;
            }
          });
        }
        if ( $.isFunction( found ) ) {
          response = found.apply(context, passedArguments);
        }
        else if(found !== undefined) {
          response = found;
        }
        if($.isArray(returnedValue)) {
          returnedValue.push(response);
        }
        else if(returnedValue !== undefined) {
          returnedValue = [returnedValue, response];
        }
        else if(response !== undefined) {
          returnedValue = response;
        }
        return found;
      }
    };

    if(methodInvoked) {
      if(instance === undefined) {
        module.initialize();
      }
      module.invoke(query);
    }
    else {
      if(instance !== undefined) {
        module.destroy();
      }
      module.initialize();
    }
  })
;

return (returnedValue !== undefined)
  ? returnedValue
  : this
;

};

$.api.settings = {

name            : 'API',
namespace       : 'api',

debug           : true,
verbose         : true,
performance     : true,

// event binding
on              : 'auto',
filter          : '.disabled',
stateContext    : false,

// state
loadingDuration : 0,
errorDuration   : 2000,

// templating
action          : false,
url             : false,
base            : '',

// data
urlData         : {},

// ui
defaultData     : true,
serializeForm   : false,
throttle        : 0,

// jQ ajax
method          : 'get',
data            : {},
dataType        : 'json',

// callbacks
beforeSend  : function(settings) { return settings; },
beforeXHR   : function(xhr) {},

onSuccess   : function(response, $module) {},
onComplete  : function(response, $module) {},
onFailure   : function(errorMessage, $module) {},
onError     : function(errorMessage, $module) {},
onAbort     : function(errorMessage, $module) {},

successTest : false,

// errors
error : {
  beforeSend        : 'The before send function has aborted the request',
  error             : 'There was an error with your request',
  exitConditions    : 'API Request Aborted. Exit conditions met',
  JSONParse         : 'JSON could not be parsed during error handling',
  legacyParameters  : 'You are using legacy API success callback names',
  method            : 'The method you called is not defined',
  missingAction     : 'API action used but no url was defined',
  missingSerialize  : 'Required dependency jquery-serialize-object missing, using basic serialize',
  missingURL        : 'No URL specified for api event',
  noReturnedValue   : 'The beforeSend callback must return a settings object, beforeSend ignored.',
  parseError        : 'There was an error parsing your request',
  requiredParameter : 'Missing a required URL parameter: ',
  statusMessage     : 'Server gave an error: ',
  timeout           : 'Your request timed out'
},

regExp  : {
  required: /\{\$*[A-z0-9]+\}/g,
  optional: /\{\/\$*[A-z0-9]+\}/g,
},

className: {
  loading : 'loading',
  error   : 'error'
},

selector: {
  form: 'form'
},

metadata: {
  action  : 'action',
  request : 'request',
  xhr     : 'xhr'
}

};

$.api.settings.api = {};

})( jQuery, window , document );