(function() {

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

module.provider('Restangular', function() {

  var helpers= {
    find: function(collection, callback, thisArg){
      var result = [];
      if (angular.isArray(collection)) {
        
        var index = -1,
            length = collection.length;

        while (++index < length) {
          var value = collection[index];
          if (callback(value)) {
            result.push(value);
          }
        }
      } else {
        angular.forEach(collection, function(value, index, collection) {
          if (callback(value)) {
            result.push(value);
          }
        });
        return result;
      }
    },
    isNull: function(value){
      return value === null;
    },
    isBoolean: function(value){
      return value === true || value === false ||
        value && typeof value == 'object' && toString.call(value) == '[object Boolean]' || false;
    },
    isEmpty: function(value){
      var result = true;
      if (!value || helpers.isNull(value)) {
        return result;
      }
      if(angular.isString(value) || angular.isArray(value)){
        return !value.length;
      }
      if(angular.isObject(value)){
        return !Object.keys(value).length
      }
      return result;
    },
    bind: function(fn, self){
      var curryArgs = arguments.length > 2 ? Array.prototype.slice.call(arguments, 2) : [];
      if (angular.isFunction(fn) && !(fn instanceof RegExp)) {
        return curryArgs.length
          ? function() {
              return arguments.length
                ? fn.apply(self, curryArgs.concat(Array.prototype.slice.call(arguments, 0)))
                : fn.apply(self, curryArgs);
            }
          : function() {
              return arguments.length
                ? fn.apply(self, arguments)
                : fn.call(self);
            };
      } else {
        // in IE, native methods are not functions so they cannot be bound (note: they don't need to be)
        return fn;
      }
    },
    contains: function(collection, value){
      if(angular.isArray(collection)){
        return collection.indexOf(value)!==-1;
      }
      if(angular.isObject(collection)){
        angular.forEach(collection, function(coll){
          if(angular.equals(coll, value)){
            return true;
          }
        })
      }
      return false;
    },
    initial: function(arr){
      return arr.slice(0,arr.length-1);
    },
    last: function(arr){
      return arr[arr.length-1];
    },
    values: function(object){
      var result= [];
      for(var key in object) {
        result.push(object[key]);
      }
      return result;
    },
    choose: function(object, predicate, pick){
      var result= {};
      predicate= (angular.isString(predicate)) ? [predicate] : predicate;
      for(var key in object) {
        if((pick && predicate.indexOf(key)!==-1) || (!pick && predicate.indexOf(key)===-1)){
          result[key]= object[key];
        }
      }
      return result;
    },
    pick: function(object, predicate){
      return helpers.choose(object, predicate, true);
    },
    omit: function(object, predicate){
      return helpers.choose(object, predicate, false);
    },
    union: function(arr1, arr2){
      var result= angular.extend([], arr1);

      angular.forEach(arr2, function(elem){
        if(result.indexOf(elem)===-1){
          result.push(elem);
        }
      });
      return result;
    },
    defaults: function(){
      var result= {};
      var args= Array.prototype.slice.call(arguments);
      angular.forEach(args, function(arg){
        angular.forEach(arg, function(){

        });
      });
      return result;
    }
  }

  // Configuration
  var Configurer = {};
  Configurer.init = function(object, config) {
    object.configuration = config;

    /**
     * Those are HTTP safe methods for which there is no need to pass any data with the request.
     */
    var safeMethods= ['get', 'head', 'options', 'trace', 'getlist'];
    config.isSafe = function(operation) {
      return helpers.contains(safeMethods, operation.toLowerCase());
    };

    var absolutePattern = /^https?:\/\//i;
    config.isAbsoluteUrl = function(string) {
      return angular.isUndefined(config.absoluteUrl) || helpers.isNull(config.absoluteUrl) ?
              string && absolutePattern.test(string) :
              config.absoluteUrl;
    };

    config.absoluteUrl = angular.isUndefined(config.absoluteUrl) ? true : config.absoluteUrl;
    object.setSelfLinkAbsoluteUrl = function(value) {
      config.absoluteUrl = value;
    };
    /**
     * This is the BaseURL to be used with Restangular
     */
    config.baseUrl = angular.isUndefined(config.baseUrl) ? '' : config.baseUrl;
    object.setBaseUrl = function(newBaseUrl) {
      config.baseUrl = /\/$/.test(newBaseUrl) ?
        newBaseUrl.substring(0, newBaseUrl.length-1) :
        newBaseUrl;
      return this;
    };

    /**
     * Sets the extra fields to keep from the parents
     */
    config.extraFields = config.extraFields || [];
    object.setExtraFields = function(newExtraFields) {
      config.extraFields = newExtraFields;
      return this;
    };

    /**
     * Some default $http parameter to be used in EVERY call
    **/
    config.defaultHttpFields = config.defaultHttpFields || {};
    object.setDefaultHttpFields = function(values) {
      config.defaultHttpFields = values;
      return this;
    };

    config.withHttpValues = function(httpLocalConfig, obj) {
      return _.defaults(obj, httpLocalConfig, config.defaultHttpFields);
    };

    config.encodeIds = angular.isUndefined(config.encodeIds) ? true : config.encodeIds;
    object.setEncodeIds = function(encode) {
      config.encodeIds = encode;
    };

    config.defaultRequestParams = config.defaultRequestParams || {
      get: {},
      post: {},
      put: {},
      remove: {},
      common: {}
    };

    object.setDefaultRequestParams = function(param1, param2) {
      var methods = [],
          params = param2 || param1;
      if (!angular.isUndefined(param2)) {
        if (angular.isArray(param1)) {
          methods = param1;
        } else {
          methods.push(param1);
        }
      } else {
        methods.push('common');
      }

      angular.forEach(methods, function (method) {
        config.defaultRequestParams[method] = params;
      });
      return this;
    };

    object.requestParams = config.defaultRequestParams;

    config.defaultHeaders = config.defaultHeaders || {};
    object.setDefaultHeaders = function(headers) {
      config.defaultHeaders = headers;
      object.defaultHeaders = config.defaultHeaders;
      return this;
    };

    object.defaultHeaders = config.defaultHeaders;

    /**
     * Method overriders will set which methods are sent via POST with an X-HTTP-Method-Override
     **/
    config.methodOverriders = config.methodOverriders || [];
    object.setMethodOverriders = function(values) {
      var overriders = angular.extend([], values);
      if (config.isOverridenMethod('delete', overriders)) {
        overriders.push('remove');
      }
      config.methodOverriders = overriders;
      return this;
    };

    config.jsonp = angular.isUndefined(config.jsonp) ? false : config.jsonp;
    object.setJsonp = function(active) {
      config.jsonp = active;
    };

    config.isOverridenMethod = function(method, values) {
      var search = values || config.methodOverriders;
      return !angular.isUndefined(helpers.find(search, function(one) {
        return one.toLowerCase() === method.toLowerCase();
      }));
    };

    /**
     * Sets the URL creator type. For now, only Path is created. In the future we'll have queryParams
    **/
    config.urlCreator = config.urlCreator || 'path';
    object.setUrlCreator = function(name) {
      if (!config.urlCreatorFactory.hasOwnProperty(name)) {
        throw new Error('URL Path selected isn\'t valid');
      }

      config.urlCreator = name;
      return this;
    };

    /**
     * You can set the restangular fields here. The 3 required fields for Restangular are:
     *
     * id: Id of the element
     * route: name of the route of this element
     * parentResource: the reference to the parent resource
     *
     *  All of this fields except for id, are handled (and created) by helpers. By default,
     *  the field values will be id, route and parentResource respectively
     */
    config.restangularFields = config.restangularFields || {
      id: 'id',
      route: 'route',
      parentResource: 'parentResource',
      restangularCollection: 'restangularCollection',
      cannonicalId: '__cannonicalId',
      etag: 'restangularEtag',
      selfLink: 'href',
      get: 'get',
      getList: 'getList',
      put: 'put',
      post: 'post',
      remove: 'remove',
      head: 'head',
      trace: 'trace',
      options: 'options',
      patch: 'patch',
      getRestangularUrl: 'getRestangularUrl',
      getRequestedUrl: 'getRequestedUrl',
      putElement: 'putElement',
      addRestangularMethod: 'addRestangularMethod',
      getParentList: 'getParentList',
      clone: 'clone',
      ids: 'ids',
      httpConfig: '_$httpConfig',
      reqParams: 'reqParams',
      one: 'one',
      all: 'all',
      several: 'several',
      oneUrl: 'oneUrl',
      allUrl: 'allUrl',
      customPUT: 'customPUT',
      customPOST: 'customPOST',
      customDELETE: 'customDELETE',
      customGET: 'customGET',
      customGETLIST: 'customGETLIST',
      customOperation: 'customOperation',
      doPUT: 'doPUT',
      doPOST: 'doPOST',
      doDELETE: 'doDELETE',
      doGET: 'doGET',
      doGETLIST: 'doGETLIST',
      fromServer: 'fromServer',
      withConfig: 'withConfig',
      withHttpConfig: 'withHttpConfig',
      singleOne: 'singleOne',
      plain: 'plain',
      save: 'save',
      restangularized: 'restangularized'
    };
    object.setRestangularFields = function(resFields) {
      config.restangularFields =
        angular.extend(config.restangularFields, resFields);
      return this;
    };

    config.isRestangularized = function(obj) {
      return !!obj[config.restangularFields.restangularized];
    };

    config.setFieldToElem = function(field, elem, value) {
      var properties = field.split('.');
      var idValue = elem;
      angular.forEach(helpers.initial(properties), function(prop) {
        idValue[prop] = {};
        idValue = idValue[prop];
      });
      idValue[helpers.last(properties)] = value;
      return this;
    };

    config.getFieldFromElem = function(field, elem) {
      var properties = field.split('.');
      var idValue = elem;
      angular.forEach(properties, function(prop) {
        if (idValue) {
          idValue = idValue[prop];
        }
      });
      return angular.copy(idValue);
    };

    config.setIdToElem = function(elem, id /*, route */) {
      config.setFieldToElem(config.restangularFields.id, elem, id);
      return this;
    };

    config.getIdFromElem = function(elem) {
      return config.getFieldFromElem(config.restangularFields.id, elem);
    };

    config.isValidId = function(elemId) {
      return '' !== elemId && !angular.isUndefined(elemId) && !helpers.isNull(elemId);
    };

    config.setUrlToElem = function(elem, url /*, route */) {
      config.setFieldToElem(config.restangularFields.selfLink, elem, url);
      return this;
    };

    config.getUrlFromElem = function(elem) {
      return config.getFieldFromElem(config.restangularFields.selfLink, elem);
    };

    config.useCannonicalId = angular.isUndefined(config.useCannonicalId) ? false : config.useCannonicalId;
    object.setUseCannonicalId = function(value) {
      config.useCannonicalId = value;
      return this;
    };

    config.getCannonicalIdFromElem = function(elem) {
      var cannonicalId = elem[config.restangularFields.cannonicalId];
      var actualId = config.isValidId(cannonicalId) ? cannonicalId : config.getIdFromElem(elem);
      return actualId;
    };

    /**
     * Sets the Response parser. This is used in case your response isn't directly the data.
     * For example if you have a response like {meta: {'meta'}, data: {name: 'Gonto'}}
     * you can extract this data which is the one that needs wrapping
     *
     * The ResponseExtractor is a function that receives the response and the method executed.
     */

    config.responseInterceptors = config.responseInterceptors || [];

    config.defaultResponseInterceptor = function(data /*, operation, what, url, response, deferred */) {
      return data;
    };

    config.responseExtractor = function(data, operation, what, url, response, deferred) {
      var interceptors = angular.copy(config.responseInterceptors);
      interceptors.push(config.defaultResponseInterceptor);
      var theData = data;
      angular.forEach(interceptors, function(interceptor) {
        theData = interceptor(theData, operation,
          what, url, response, deferred);
      });
      return theData;
    };

    object.addResponseInterceptor = function(extractor) {
      config.responseInterceptors.push(extractor);
      return this;
    };

    config.errorInterceptors = config.errorInterceptors || [];
    object.addErrorInterceptor = function(interceptor) {
      config.errorInterceptors.push(interceptor);
      return this;
    };

    object.setResponseInterceptor = object.addResponseInterceptor;
    object.setResponseExtractor = object.addResponseInterceptor;
    object.setErrorInterceptor = object.addErrorInterceptor;

    /**
     * Response interceptor is called just before resolving promises.
     */


    /**
     * Request interceptor is called before sending an object to the server.
     */
    config.requestInterceptors = config.requestInterceptors || [];

    config.defaultInterceptor = function(element, operation, path, url, headers, params, httpConfig) {
      return {
        element: element,
        headers: headers,
        params: params,
        httpConfig: httpConfig
      };
    };

    config.fullRequestInterceptor = function(element, operation, path, url, headers, params, httpConfig) {
      var interceptors = angular.copy(config.requestInterceptors);
      var defaultRequest = config.defaultInterceptor(element, operation, path, url, headers, params, httpConfig);
      return _.reduce(interceptors, function(request, interceptor) {
        return angular.extend(request, interceptor(request.element, operation,
          path, url, request.headers, request.params, request.httpConfig));
      }, defaultRequest);
    };

    object.addRequestInterceptor = function(interceptor) {
      config.requestInterceptors.push(function(elem, operation, path, url, headers, params, httpConfig) {
        return {
          headers: headers,
          params: params,
          element: interceptor(elem, operation, path, url),
          httpConfig: httpConfig
        };
      });
      return this;
    };

    object.setRequestInterceptor = object.addRequestInterceptor;

    object.addFullRequestInterceptor = function(interceptor) {
      config.requestInterceptors.push(interceptor);
      return this;
    };

    object.setFullRequestInterceptor = object.addFullRequestInterceptor;

    config.onBeforeElemRestangularized = config.onBeforeElemRestangularized || function(elem) {
      return elem;
    };
    object.setOnBeforeElemRestangularized = function(post) {
      config.onBeforeElemRestangularized = post;
      return this;
    };

    object.setRestangularizePromiseInterceptor = function(interceptor) {
      config.restangularizePromiseInterceptor = interceptor;
      return this;
    };

    /**
     * This method is called after an element has been "Restangularized".
     *
     * It receives the element, a boolean indicating if it's an element or a collection
     * and the name of the model
     *
     */
    config.onElemRestangularized = config.onElemRestangularized || function(elem) {
      return elem;
    };
    object.setOnElemRestangularized = function(post) {
      config.onElemRestangularized = post;
      return this;
    };

    config.shouldSaveParent = config.shouldSaveParent || function() {
      return true;
    };
    object.setParentless = function(values) {
      if (angular.isArray(values)) {
        config.shouldSaveParent = function(route) {
          return !helpers.contains(values, route);
        };
      } else if (helpers.isBoolean(values)) {
        config.shouldSaveParent = function() {
          return !values;
        };
      }
      return this;
    };

    /**
     * This lets you set a suffix to every request.
     *
     * For example, if your api requires that for JSon requests you do /users/123.json, you can set that
     * in here.
     *
     *
     * By default, the suffix is null
     */
    config.suffix = angular.isUndefined(config.suffix) ? null : config.suffix;
    object.setRequestSuffix = function(newSuffix) {
      config.suffix = newSuffix;
      return this;
    };

    /**
     * Add element transformers for certain routes.
     */
    config.transformers = config.transformers || {};
    object.addElementTransformer = function(type, secondArg, thirdArg) {
      var isCollection = null;
      var transformer = null;
      if (arguments.length === 2) {
        transformer = secondArg;
      } else {
        transformer = thirdArg;
        isCollection = secondArg;
      }

      var typeTransformers = config.transformers[type];
      if (!typeTransformers) {
        typeTransformers = config.transformers[type] = [];
      }

      typeTransformers.push(function(coll, elem) {
        if (helpers.isNull(isCollection) || (coll === isCollection)) {
          return transformer(elem);
        }
        return elem;
      });

      return object;
    };

    object.extendCollection = function(route, fn) {
      return object.addElementTransformer(route, true, fn);
    };

    object.extendModel = function(route, fn) {
      return object.addElementTransformer(route, false, fn);
    };

    config.transformElem = function(elem, isCollection, route, Restangular, force) {
      if (!force && !config.transformLocalElements && !elem[config.restangularFields.fromServer]) {
        return elem;
      }
      var typeTransformers = config.transformers[route];
      var changedElem = elem;
      if (typeTransformers) {
        angular.forEach(typeTransformers, function(transformer) {
          changedElem = transformer(isCollection, changedElem);
        });
      }
      return config.onElemRestangularized(changedElem, isCollection, route, Restangular);
    };

    config.transformLocalElements = angular.isUndefined(config.transformLocalElements) ?
      false :
      config.transformLocalElements;

    object.setTransformOnlyServerElements = function(active) {
      config.transformLocalElements = !active;
    };

    config.fullResponse = angular.isUndefined(config.fullResponse) ? false : config.fullResponse;
    object.setFullResponse = function(full) {
      config.fullResponse = full;
      return this;
    };


    //Internal values and functions
    config.urlCreatorFactory = {};

    /**
     * Base URL Creator. Base prototype for everything related to it
     **/

     var BaseCreator = function() {
     };

     BaseCreator.prototype.setConfig = function(config) {
       this.config = config;
       return this;
     };

     BaseCreator.prototype.parentsArray = function(current) {
      var parents = [];
      while(current) {
        parents.push(current);
        current = current[this.config.restangularFields.parentResource];
      }
      return parents.reverse();
    };

    function RestangularResource(config, $http, url, configurer) {
      var resource = {};
      angular.forEach(Object.keys(configurer), function(key) {
        var value = configurer[key];

        // Add default parameters
        value.params = angular.extend({}, value.params, config.defaultRequestParams[value.method.toLowerCase()]);
        // We don't want the ? if no params are there
        if (helpers.isEmpty(value.params)) {
          delete value.params;
        }

        if (config.isSafe(value.method)) {

          resource[key] = function() {
            return $http(angular.extend(value, {
              url: url
            }));
          };

        } else {

          resource[key] = function(data) {
            return $http(angular.extend(value, {
              url: url,
              data: data
            }));
          };

        }
      });

      return resource;
    }

    BaseCreator.prototype.resource = function(current, $http, localHttpConfig, callHeaders, callParams, what, etag,operation) {

      var params = _.defaults(callParams || {}, this.config.defaultRequestParams.common);
      var headers = _.defaults(callHeaders || {}, this.config.defaultHeaders);

      if (etag) {
        if (!config.isSafe(operation)) {
          headers['If-Match'] = etag;
        } else {
          headers['If-None-Match'] = etag;
        }
      }

      var url = this.base(current);

      if (what) {
        var add = '';
        if (!/\/$/.test(url)) {
          add += '/';
        }
        add += what;
        url += add;
      }

      if (this.config.suffix &&
        url.indexOf(this.config.suffix, url.length - this.config.suffix.length) === -1 &&
        !this.config.getUrlFromElem(current)) {
          url += this.config.suffix;
      }

      current[this.config.restangularFields.httpConfig] = undefined;

      return RestangularResource(this.config, $http, url, {
        getList: this.config.withHttpValues(localHttpConfig,
          {method: 'GET',
          params: params,
          headers: headers}),

        get: this.config.withHttpValues(localHttpConfig,
          {method: 'GET',
          params: params,
          headers: headers}),

        jsonp: this.config.withHttpValues(localHttpConfig,
          {method: 'jsonp',
          params: params,
          headers: headers}),

        put: this.config.withHttpValues(localHttpConfig,
          {method: 'PUT',
          params: params,
          headers: headers}),

        post: this.config.withHttpValues(localHttpConfig,
          {method: 'POST',
          params: params,
          headers: headers}),

        remove: this.config.withHttpValues(localHttpConfig,
          {method: 'DELETE',
          params: params,
          headers: headers}),

        head: this.config.withHttpValues(localHttpConfig,
          {method: 'HEAD',
          params: params,
          headers: headers}),

        trace: this.config.withHttpValues(localHttpConfig,
          {method: 'TRACE',
          params: params,
          headers: headers}),

        options: this.config.withHttpValues(localHttpConfig,
          {method: 'OPTIONS',
          params: params,
          headers: headers}),

        patch: this.config.withHttpValues(localHttpConfig,
          {method: 'PATCH',
          params: params,
          headers: headers})
      });
    };

    /**
     * This is the Path URL creator. It uses Path to show Hierarchy in the Rest API.
     * This means that if you have an Account that then has a set of Buildings, a URL to a building
     * would be /accounts/123/buildings/456
    **/
    var Path = function() {
    };

    Path.prototype = new BaseCreator();

    Path.prototype.base = function(current) {
      var __this = this;
      return  _.reduce(this.parentsArray(current), function(acum, elem) {
        var elemUrl;
        var elemSelfLink = __this.config.getUrlFromElem(elem);
        if (elemSelfLink) {
          if (__this.config.isAbsoluteUrl(elemSelfLink)) {
            return elemSelfLink;
          } else {
            elemUrl = elemSelfLink;
          }
        } else {
          elemUrl = elem[__this.config.restangularFields.route];

          if (elem[__this.config.restangularFields.restangularCollection]) {
            var ids = elem[__this.config.restangularFields.ids];
            if (ids) {
              elemUrl += '/' + ids.join(',');
            }
          } else {
            var elemId;
            if (__this.config.useCannonicalId) {
              elemId = __this.config.getCannonicalIdFromElem(elem);
            } else {
              elemId = __this.config.getIdFromElem(elem);
            }

            if (config.isValidId(elemId) && !elem.singleOne) {
              elemUrl += '/' + (__this.config.encodeIds ? encodeURIComponent(elemId) : elemId);
            }
          }
        }

        return acum.replace(/\/$/, '') + '/' + elemUrl;

      }, this.config.baseUrl);
    };



    Path.prototype.fetchUrl = function(current, what) {
      var baseUrl = this.base(current);
      if (what) {
        baseUrl += '/' + what;
      }
      return baseUrl;
    };

    Path.prototype.fetchRequestedUrl = function(current, what) {
      var url = this.fetchUrl(current, what);
      var params = current[config.restangularFields.reqParams];

      // From here on and until the end of fetchRequestedUrl,
      // the code has been kindly borrowed from angular.js
      // The reason for such code bloating is coherence:
      //   If the user were to use this for cache management, the
      //   serialization of parameters would need to be identical
      //   to the one done by angular for cache keys to match.
      function sortedKeys(obj) {
        var keys = [];
        for (var key in obj) {
          if (obj.hasOwnProperty(key)) {
            keys.push(key);
          }
        }
        return keys.sort();
      }

      function forEachSorted(obj, iterator, context) {
        var keys = sortedKeys(obj);
        for ( var i = 0; i < keys.length; i++) {
          iterator.call(context, obj[keys[i]], keys[i]);
        }
        return keys;
      }

      function encodeUriQuery(val, pctEncodeSpaces) {
        return encodeURIComponent(val).
                   replace(/%40/gi, '@').
                   replace(/%3A/gi, ':').
                   replace(/%24/g, '$').
                   replace(/%2C/gi, ',').
                   replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
      }

      if (!params) { return url; }

      var parts = [];
      forEachSorted(params, function(value, key) {
        if (value === null || value === undefined) { return; }
        if (!angular.isArray(value)) { value = [value]; }

        angular.forEach(value, function(v) {
          if (angular.isObject(v)) {
            v = angular.toJson(v);
          }
          parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(v));
        });
      });

      return url + (this.config.suffix || '') + ((url.indexOf('?') === -1) ? '?' : '&') + parts.join('&');
    };

    config.urlCreatorFactory.path = Path;
  };

  var globalConfiguration = {};

  Configurer.init(this, globalConfiguration);



  this.$get = ['$http', '$q', function($http, $q) {

    function createServiceForConfiguration(config) {
      var service = {};

      var urlHandler = new config.urlCreatorFactory[config.urlCreator]();
      urlHandler.setConfig(config);

      function restangularizeBase(parent, elem, route, reqParams, fromServer) {
        elem[config.restangularFields.route] = route;
        elem[config.restangularFields.getRestangularUrl] = helpers.bind(urlHandler.fetchUrl, urlHandler, elem);
        elem[config.restangularFields.getRequestedUrl] = helpers.bind(urlHandler.fetchRequestedUrl, urlHandler, elem);
        elem[config.restangularFields.addRestangularMethod] = helpers.bind(addRestangularMethodFunction, elem);
        elem[config.restangularFields.clone] = helpers.bind(copyRestangularizedElement, elem, elem);
        elem[config.restangularFields.reqParams] = helpers.isEmpty(reqParams) ? null : reqParams;
        elem[config.restangularFields.withHttpConfig] = helpers.bind(withHttpConfig, elem);
        elem[config.restangularFields.plain] = helpers.bind(stripRestangular, elem, elem);

        // Tag element as restangularized
        elem[config.restangularFields.restangularized] = true;

        // RequestLess connection
        elem[config.restangularFields.one] = helpers.bind(one, elem, elem);
        elem[config.restangularFields.all] = helpers.bind(all, elem, elem);
        elem[config.restangularFields.several] = helpers.bind(several, elem, elem);
        elem[config.restangularFields.oneUrl] = helpers.bind(oneUrl, elem, elem);
        elem[config.restangularFields.allUrl] = helpers.bind(allUrl, elem, elem);

        elem[config.restangularFields.fromServer] = !!fromServer;

        if (parent && config.shouldSaveParent(route)) {
          var parentId = config.getIdFromElem(parent);
          var parentUrl = config.getUrlFromElem(parent);

          var restangularFieldsForParent = helpers.union(
            helpers.values(helpers.pick(config.restangularFields, ['route', 'singleOne', 'parentResource']) ),
            config.extraFields
          );
          
          var parentResource = helpers.pick(parent, restangularFieldsForParent);
          
          if (config.isValidId(parentId)) {
            config.setIdToElem(parentResource, parentId, route);
          }
          if (config.isValidId(parentUrl)) {
            config.setUrlToElem(parentResource, parentUrl, route);
          }

          elem[config.restangularFields.parentResource] = parentResource;
        } else {
          elem[config.restangularFields.parentResource] = null;
        }
        return elem;
      }

      function one(parent, route, id, singleOne) {
        if (angular.isNumber(route) || angular.isNumber(parent)) {
          var error = 'You\'re creating a Restangular entity with the number ';
          error += 'instead of the route or the parent. For example, you can\'t call .one(12)';
          throw new Error(error);
        }
        var elem = {};
        config.setIdToElem(elem, id, route);
        config.setFieldToElem(config.restangularFields.singleOne, elem, singleOne);
        return restangularizeElem(parent, elem , route, false);
      }


      function all(parent, route) {
        return restangularizeCollection(parent, [] , route, false);
      }

      function several(parent, route /*, ids */) {
        var collection = [];
        collection[config.restangularFields.ids] = Array.prototype.splice.call(arguments, 2);
        return restangularizeCollection(parent, collection , route, false);
      }

      function oneUrl(parent, route, url) {
        if (!route) {
          throw new Error('Route is mandatory when creating new Restangular objects.');
        }
        var elem = {};
        config.setUrlToElem(elem, url, route);
        return restangularizeElem(parent, elem , route, false);
      }


      function allUrl(parent, route, url) {
        if (!route) {
          throw new Error('Route is mandatory when creating new Restangular objects.');
        }
        var elem = {};
        config.setUrlToElem(elem, url, route);
        return restangularizeCollection(parent, elem , route, false);
      }
      // Promises
      function restangularizePromise(promise, isCollection, valueToFill) {
        promise.call = helpers.bind(promiseCall, promise);
        promise.get = helpers.bind(promiseGet, promise);
        promise[config.restangularFields.restangularCollection] = isCollection;
        if (isCollection) {
            promise.push = helpers.bind(promiseCall, promise, 'push');
        }
        promise.$object = valueToFill;
        if (config.restangularizePromiseInterceptor) {
          config.restangularizePromiseInterceptor(promise);
        }
        return promise;
      }

      function promiseCall(method) {
        var deferred = $q.defer();
        var callArgs = arguments;
        var filledValue = {};
        this.then(function(val) {
          var params = Array.prototype.slice.call(callArgs, 1);
          var func = val[method];
          func.apply(val, params);
          filledValue = val;
          deferred.resolve(val);
        });
        return restangularizePromise(deferred.promise, this[config.restangularFields.restangularCollection], filledValue);
      }

      function promiseGet(what) {
        var deferred = $q.defer();
        var filledValue = {};
        this.then(function(val) {
          filledValue = val[what];
          deferred.resolve(filledValue);
        });
        return restangularizePromise(deferred.promise, this[config.restangularFields.restangularCollection], filledValue);
      }

      function resolvePromise(deferred, response, data, filledValue) {
        angular.extend(filledValue, data);

        // Trigger the full response interceptor.
        if (config.fullResponse) {
          return deferred.resolve(angular.extend(response, {
            data: data
          }));
        } else {
          deferred.resolve(data);
        }
      }


      // Elements
      function stripRestangular(elem) {
        if (angular.isArray(elem)) {
          var array = [];
          angular.forEach(elem, function(value) {
              array.push(config.isRestangularized(value) ?  stripRestangular(value) : value);
          });
          return array;
        } else {
          return _.omit(elem, helpers.values(_.omit(config.restangularFields, 'id')));
        }
      }

      function addCustomOperation(elem) {
        elem[config.restangularFields.customOperation] = helpers.bind(customFunction, elem);
        angular.forEach(['put', 'post', 'get', 'delete'], function(oper) {
          angular.forEach(['do', 'custom'], function(alias) {
            var callOperation = oper === 'delete' ? 'remove' : oper;
            var name = alias + oper.toUpperCase();
            var callFunction;

            if (callOperation !== 'put' && callOperation !== 'post') {
              callFunction = customFunction;
            } else {
              callFunction = function(operation, elem, path, params, headers) {
                return helpers.bind(customFunction, this)(operation, path, params, headers, elem);
              };
            }
            elem[name] = helpers.bind(callFunction, elem, callOperation);
          });
        });
        elem[config.restangularFields.customGETLIST] = helpers.bind(fetchFunction, elem);
        elem[config.restangularFields.doGETLIST] = elem[config.restangularFields.customGETLIST];
      }

      function copyRestangularizedElement(fromElement, toElement) {
        var copiedElement = angular.copy(fromElement, toElement);
        return restangularizeElem(copiedElement[config.restangularFields.parentResource],
                copiedElement, copiedElement[config.restangularFields.route], true);
      }

      function restangularizeElem(parent, element, route, fromServer, collection, reqParams) {
        var elem = config.onBeforeElemRestangularized(element, false, route);

        var localElem = restangularizeBase(parent, elem, route, reqParams, fromServer);

        if (config.useCannonicalId) {
          localElem[config.restangularFields.cannonicalId] = config.getIdFromElem(localElem);
        }

        if (collection) {
          localElem[config.restangularFields.getParentList] = function() {
            return collection;
          };
        }

        localElem[config.restangularFields.restangularCollection] = false;
        localElem[config.restangularFields.get] = helpers.bind(getFunction, localElem);
        localElem[config.restangularFields.getList] = helpers.bind(fetchFunction, localElem);
        localElem[config.restangularFields.put] = helpers.bind(putFunction, localElem);
        localElem[config.restangularFields.post] = helpers.bind(postFunction, localElem);
        localElem[config.restangularFields.remove] = helpers.bind(deleteFunction, localElem);
        localElem[config.restangularFields.head] = helpers.bind(headFunction, localElem);
        localElem[config.restangularFields.trace] = helpers.bind(traceFunction, localElem);
        localElem[config.restangularFields.options] = helpers.bind(optionsFunction, localElem);
        localElem[config.restangularFields.patch] = helpers.bind(patchFunction, localElem);
        localElem[config.restangularFields.save] = helpers.bind(save, localElem);

        addCustomOperation(localElem);
        return config.transformElem(localElem, false, route, service, true);
      }

      function restangularizeCollection(parent, element, route, fromServer, reqParams) {
        var elem = config.onBeforeElemRestangularized(element, true, route);

        var localElem = restangularizeBase(parent, elem, route, reqParams, fromServer);
        localElem[config.restangularFields.restangularCollection] = true;
        localElem[config.restangularFields.post] = helpers.bind(postFunction, localElem, null);
        localElem[config.restangularFields.remove] = helpers.bind(deleteFunction, localElem);
        localElem[config.restangularFields.head] = helpers.bind(headFunction, localElem);
        localElem[config.restangularFields.trace] = helpers.bind(traceFunction, localElem);
        localElem[config.restangularFields.putElement] = helpers.bind(putElementFunction, localElem);
        localElem[config.restangularFields.options] = helpers.bind(optionsFunction, localElem);
        localElem[config.restangularFields.patch] = helpers.bind(patchFunction, localElem);
        localElem[config.restangularFields.get] = helpers.bind(getById, localElem);
        localElem[config.restangularFields.getList] = helpers.bind(fetchFunction, localElem, null);

        addCustomOperation(localElem);
        return config.transformElem(localElem, true, route, service, true);
      }

      function restangularizeCollectionAndElements(parent, element, route) {
        var collection = restangularizeCollection(parent, element, route, false);
        angular.forEach(collection, function(elem) {
          restangularizeElem(parent, elem, route, false);
        });
        return collection;
      }

      function getById(id, reqParams, headers){
        return this.customGET(id.toString(), reqParams, headers);
      }

      function putElementFunction(idx, params, headers) {
        var __this = this;
        var elemToPut = this[idx];
        var deferred = $q.defer();
        var filledArray = [];
        filledArray = config.transformElem(filledArray, true, elemToPut[config.restangularFields.route], service);
        elemToPut.put(params, headers).then(function(serverElem) {
          var newArray = copyRestangularizedElement(__this);
          newArray[idx] = serverElem;
          filledArray = newArray;
          deferred.resolve(newArray);
        }, function(response) {
          deferred.reject(response);
        });

        return restangularizePromise(deferred.promise, true, filledArray);
      }

      function parseResponse(resData, operation, route, fetchUrl, response, deferred) {
        var data = config.responseExtractor(resData, operation, route, fetchUrl, response, deferred);
        var etag = response.headers('ETag');
        if (data && etag) {
          data[config.restangularFields.etag] = etag;
        }
        return data;
      }


      function fetchFunction(what, reqParams, headers) {
        var __this = this;
        var deferred = $q.defer();
        var operation = 'getList';
        var url = urlHandler.fetchUrl(this, what);
        var whatFetched = what || __this[config.restangularFields.route];

        var request = config.fullRequestInterceptor(null, operation,
            whatFetched, url, headers || {}, reqParams || {}, this[config.restangularFields.httpConfig] || {});

        var filledArray = [];
        filledArray = config.transformElem(filledArray, true, whatFetched, service);

        var method = 'getList';

        if (config.jsonp) {
          method = 'jsonp';
        }

        var okCallback = function(response) {
          var resData = response.data;
          var fullParams = response.config.params;
          var data = parseResponse(resData, operation, whatFetched, url, response, deferred);

          // support empty response for getList() calls (some APIs respond with 204 and empty body)
          if (angular.isUndefined(data) || '' === data) {
            data = [];
          }
          if (!angular.isArray(data)) {
            throw new Error('Response for getList SHOULD be an array and not an object or something else');
          }
          var processedData = data.map(function(elem) {
            if (!__this[config.restangularFields.restangularCollection]) {
              return restangularizeElem(__this, elem, what, true, data);
            } else {
              return restangularizeElem(__this[config.restangularFields.parentResource],
                elem, __this[config.restangularFields.route], true, data);
            }
          });

          processedData = angular.extend(data, processedData);

          if (!__this[config.restangularFields.restangularCollection]) {
            resolvePromise(
              deferred,
              response,
              restangularizeCollection(
                __this,
                processedData,
                what,
                true,
                fullParams
              ),
              filledArray
            );
          } else {
            resolvePromise(
              deferred,
              response,
              restangularizeCollection(
                __this[config.restangularFields.parentResource],
                processedData,
                __this[config.restangularFields.route],
                true,
                fullParams
              ),
              filledArray
            );
          }
        };

        urlHandler.resource(this, $http, request.httpConfig, request.headers, request.params, what,
                this[config.restangularFields.etag], operation)[method]().then(okCallback, function error(response) {
          if (response.status === 304 && __this[config.restangularFields.restangularCollection]) {
            resolvePromise(deferred, response, __this, filledArray);
          } else if ( _.every(config.errorInterceptors, function(cb) { return cb(response, deferred, okCallback) !== false; }) ) {
            // triggered if no callback returns false
            deferred.reject(response);
          }
        });

        return restangularizePromise(deferred.promise, true, filledArray);
      }

      function withHttpConfig(httpConfig) {
        this[config.restangularFields.httpConfig] = httpConfig;
        return this;
      }

      function save(params, headers) {
        if (this[config.restangularFields.fromServer]) {
          return this[config.restangularFields.put](params, headers);
        } else {
          return helpers.bind(elemFunction, this)('post', undefined, params, undefined, headers);
        }
      }

      function elemFunction(operation, what, params, obj, headers) {
        var __this = this;
        var deferred = $q.defer();
        var resParams = params || {};
        var route = what || this[config.restangularFields.route];
        var fetchUrl = urlHandler.fetchUrl(this, what);

        var callObj = obj || this;
        // fallback to etag on restangular object (since for custom methods we probably don't explicitly specify the etag field)
        var etag = callObj[config.restangularFields.etag] || (operation !== 'post' ? this[config.restangularFields.etag] : null);

        if (angular.isObject(callObj) && config.isRestangularized(callObj)) {
          callObj = stripRestangular(callObj);
        }
        var request = config.fullRequestInterceptor(callObj, operation, route, fetchUrl,
          headers || {}, resParams || {}, this[config.restangularFields.httpConfig] || {});

        var filledObject = {};
        filledObject = config.transformElem(filledObject, false, route, service);

        var okCallback = function(response) {
          var resData = response.data;
          var fullParams = response.config.params;
          var elem = parseResponse(resData, operation, route, fetchUrl, response, deferred);
          if (elem) {

            if (operation === 'post' && !__this[config.restangularFields.restangularCollection]) {
              resolvePromise(deferred, response, restangularizeElem(__this, elem, what, true, null, fullParams), filledObject);
            } else {
              var data = restangularizeElem(
                __this[config.restangularFields.parentResource],
                elem,
                __this[config.restangularFields.route],
                true,
                null,
                fullParams
              );

              data[config.restangularFields.singleOne] = __this[config.restangularFields.singleOne];
              resolvePromise(deferred, response, data, filledObject);
            }

          } else {
            resolvePromise(deferred, response, undefined, filledObject);
          }
        };

        var errorCallback = function(response) {
          if (response.status === 304 && config.isSafe(operation)) {
            resolvePromise(deferred, response, __this, filledObject);
          } else if ( _.every(config.errorInterceptors, function(cb) { return cb(response, deferred, okCallback) !== false; }) ) {
            // triggered if no callback returns false
            deferred.reject(response);
          }
        };
        // Overring HTTP Method
        var callOperation = operation;
        var callHeaders = angular.extend({}, request.headers);
        var isOverrideOperation = config.isOverridenMethod(operation);
        if (isOverrideOperation) {
          callOperation = 'post';
          callHeaders = angular.extend(callHeaders, {'X-HTTP-Method-Override': operation === 'remove' ? 'DELETE' : operation});
        } else if (config.jsonp && callOperation === 'get') {
          callOperation = 'jsonp';
        }

        if (config.isSafe(operation)) {
          if (isOverrideOperation) {
            urlHandler.resource(this, $http, request.httpConfig, callHeaders, request.params,
              what, etag, callOperation)[callOperation]({}).then(okCallback, errorCallback);
          } else {
            urlHandler.resource(this, $http, request.httpConfig, callHeaders, request.params,
              what, etag, callOperation)[callOperation]().then(okCallback, errorCallback);
          }
        } else {
          urlHandler.resource(this, $http, request.httpConfig, callHeaders, request.params,
            what, etag, callOperation)[callOperation](request.element).then(okCallback, errorCallback);
        }

        return restangularizePromise(deferred.promise, false, filledObject);
      }

      function getFunction(params, headers) {
        return helpers.bind(elemFunction, this)('get', undefined, params, undefined, headers);
      }

      function deleteFunction(params, headers) {
        return helpers.bind(elemFunction, this)('remove', undefined, params, undefined, headers);
      }

      function putFunction(params, headers) {
        return helpers.bind(elemFunction, this)('put', undefined, params, undefined, headers);
      }

      function postFunction(what, elem, params, headers) {
        return helpers.bind(elemFunction, this)('post', what, params, elem, headers);
      }

      function headFunction(params, headers) {
        return helpers.bind(elemFunction, this)('head', undefined, params, undefined, headers);
      }

      function traceFunction(params, headers) {
        return helpers.bind(elemFunction, this)('trace', undefined, params, undefined, headers);
      }

      function optionsFunction(params, headers) {
        return helpers.bind(elemFunction, this)('options', undefined, params, undefined, headers);
      }

      function patchFunction(elem, params, headers) {
        return helpers.bind(elemFunction, this)('patch', undefined, params, elem, headers);
      }

      function customFunction(operation, path, params, headers, elem) {
        return helpers.bind(elemFunction, this)(operation, path, params, elem, headers);
      }

      function addRestangularMethodFunction(name, operation, path, defaultParams, defaultHeaders, defaultElem) {
        var bindedFunction;
        if (operation === 'getList') {
          bindedFunction = helpers.bind(fetchFunction, this, path);
        } else {
          bindedFunction = helpers.bind(customFunction, this, operation, path);
        }

        var createdFunction = function(params, headers, elem) {
          var callParams = _.defaults({
            params: params,
            headers: headers,
            elem: elem
          }, {
            params: defaultParams,
            headers: defaultHeaders,
            elem: defaultElem
          });
          return bindedFunction(callParams.params, callParams.headers, callParams.elem);
        };

        if (config.isSafe(operation)) {
          this[name] = createdFunction;
        } else {
          this[name] = function(elem, params, headers) {
            return createdFunction(params, headers, elem);
          };
        }
      }

      function withConfigurationFunction(configurer) {
        var newConfig = angular.copy(helpers.omit(config, 'configuration'));
        Configurer.init(newConfig, newConfig);
        configurer(newConfig);
        return createServiceForConfiguration(newConfig);
      }

      function toService(route, parent) {
        var knownCollectionMethods = helpers.values(config.restangularFields);
        var serv = {};
        var collection = (parent || service).all(route);
        serv.one = helpers.bind(one, (parent || service), parent, route);
        serv.post = helpers.bind(collection.post, collection);
        serv.getList = helpers.bind(collection.getList, collection);

        for (var prop in collection) {
          if (collection.hasOwnProperty(prop) && angular.isFunction(collection[prop]) && !helpers.contains(knownCollectionMethods, prop)) {
            serv[prop] = helpers.bind(collection[prop], collection);
          }
        }

        return serv;
      }


      Configurer.init(service, config);

      service.copy = helpers.bind(copyRestangularizedElement, service);

      service.service = helpers.bind(toService, service);

      service.withConfig = helpers.bind(withConfigurationFunction, service);

      service.one = helpers.bind(one, service, null);

      service.all = helpers.bind(all, service, null);
      
      service.several = helpers.bind(several, service, null);

      service.oneUrl = helpers.bind(oneUrl, service, null);

      service.allUrl = helpers.bind(allUrl, service, null);

      service.stripRestangular = helpers.bind(stripRestangular, service);

      service.restangularizeElement = helpers.bind(restangularizeElem, service);

      service.restangularizeCollection = helpers.bind(restangularizeCollectionAndElements, service);

      return service;
    }

    return createServiceForConfiguration(globalConfiguration);
  }];
});

})();
