/**
 * @preserve
 * Jribbble v2.0.3 | Tue May 26 21:23:05 2015 -0400
 * Copyright (c) 2015, Tyler Gaw me@tylergaw.com
 * Released under the ISC-LICENSE
 */
;(function($, window, document, undefined) {
  'use strict';

  // This is our public access point.
  $.jribbble = {};

  var ACCESS_TOKEN = null;
  var API_URL = 'https://api.dribbble.com/v1';

  // The types of shot lists that are available through the API.
  // The default shot list–retrieved by shots()–is any type.
  var SHOT_LIST_TYPES = [
    'animated',
    'attachments',
    'debuts',
    'playoffs',
    'rebounds',
    'teams'
  ];

  // Rather than pepper the code with error messages, we use this object to store
  // them. There are also a number of convenience methods here for creating
  // common error messages for different resources.
  var ERROR_MSGS = {
    token: 'Jribbble: Missing Dribbble access token. Set one with ' +
      '$.jribbble.accessToken = YOUR_ACCESS_TOKEN. If you do not have an ' +
      'access token, you must register a new application at ' +
      'https://dribbble.com/account/applications/new',

    singular: function(str) {
      return str.substr(0, str.length - 1);
    },

    idRequired: function(resource) {
      return 'Jribbble: You have to provide a ' + this.singular(resource) +
        ' ID. ex: $.jribbble.%@("1234").'.replace(/%@/g, resource);
    },

    subResource: function(resource) {
      return 'Jribbble: You have to provide a ' + this.singular(resource) +
        ' ID to get %@. ex: $.jribbble.%@("1234").%@()'.replace(/%@/g, resource);
    },

    // A shot ID is required to get shot sub-resources.
    shotId: function(resource) {
      return 'Jribbble: You have to provide a shot ID to get %@. ex: ' +
        ' $.jribbble.shots("1234").%@()'.replace(/%@/g, resource);
    },

    commentLikes: 'Jribbble: You have to provide a comment ID to get likes. ex: ' +
      ' $.jribbble.shots("1234").comments("456").likes()'
  };

  // A number of resources do not allow for bare calls to them, they require a
  // resource ID. If the ID is not provided, things will not function so we err.
  var checkId = function(id, resource) {
    if (!id || typeof id === 'object') {
      throw new Error(ERROR_MSGS.idRequired(resource));
    } else {
      return id;
    }
  };

  // Many top-level resources–users,buckets, etc–have subresources that all behave
  // in a similar way. We use this function to create new methods on the top-level
  // object's prototype.
  var createSubResources = function(subResources) {
    var obj = {};

    subResources.forEach(function(resource) {
      obj[resource] = subResourceWithOpts.call(this, resource);
    }.bind(this));

    return obj;
  };

  // Provide an object of key: value params. Get back a URL encoded string if
  // params has keys.
  var parseParams = function(params) {
    var p = $.param(params);

    if (p) {
      return '?' + p;
    } else {
      return '';
    }
  };

  // Because we want our API to be as flexible as possible, there are a number
  // of methods where we'll allow different types of arguments to be passed as
  // the first argument and then we'll figure out here what the user meant.
  var negotiateArgs = function(args) {
    // If there's nothing here, just bail, that's OK. Calls like `shots()` are
    // fine on their own.
    if (args.length !== 0) {
      var firstArg = args[0];
      var type = typeof firstArg;
      var params = {};

      // If the first argument is a number or string, we're going to assume that
      // the user wants a resource by id or name.
      if (type === 'number' || type === 'string') {
        var list = SHOT_LIST_TYPES.indexOf(firstArg);

        // Shots can be retrieved by the shot name. The Dribbble API wants this
        // passed as a query paramter, but as a conveinence, you can pass the
        // name of a shot list to shots() and we'll check here if that name
        // is a valid shot list name.
        if (list > -1) {
          params.list = firstArg;
        } else {
          params.resource = firstArg;
        }
      // If we see an object as the first parameter, we assume the user is
      // providing options to be passed as query parameters.
      } else if (type === 'object') {
        params = firstArg;
      }

      return params;
    }
  };

  // All initial Jribbble API methods–shots, buckets, project, etc–share common
  // functionality. We mix this base functionality into each one.
  var jribbbleBase = function() {
    var ext = $.extend({}, $.Deferred());

    var Queue = function() {
      this.methods = [];
      this.response = null;
      this.flushed = false;

      this.add = function(fn) {
        if (this.flushed) {
          fn(this.scope);
        } else {
          this.methods.push(fn);
        }
      };

      this.flush = function(scope) {
        if (this.flushed) {
          return;
        }

        this.scope = scope;
        this.flushed = true;

        while(this.methods[0]) {
          this.methods.shift()(scope);
        }

        return scope;
      };

      return this;
    };

    ext.queue = new Queue();
    ext.url = API_URL;

    ext.get = function() {
      if (!ACCESS_TOKEN) {
        console.error(ERROR_MSGS.token);

        return false;
      }

      $.ajax({
        type: 'GET',
        url: this.url,
        beforeSend: function(jqxhr) {
          jqxhr.setRequestHeader('Authorization', 'Bearer ' + ACCESS_TOKEN);
        },
        success: function(res) {
          this.resolve(res);
        }.bind(this),
        error: function(jqxhr) {
          this.reject(jqxhr);
        }.bind(this)
      });

      return this;
    };

    return ext;
  };

  // Because a number of API resources are set up the same way, we can create
  // new Jribble API methods for them using currying. This extends the method
  // with the JribbbleBase, adds a method to the queue, and sets the needed
  // timeout for flushing the queue.
  // See jribbble.buckets for example usage.
  var resourceWithoutOpts = function(resource) {
    return function(resourceId) {
      $.extend(this, jribbbleBase());

      this.queue.add(function(self) {
        self.url += '/' + resource + '/' + resourceId;
      });

      setTimeout(function() {
        this.queue.flush(this).get();
      }.bind(this));

      return this;
    };
  };

  // Because a number of API resources are set up the same way, we can create
  // new Jribble API methods for them using currying. This function returns a
  // function that allows for creating URLS like:
  // /resource/subresource/?foo=1&bar=2
  var subResourceWithOpts = function(resource) {
    return function(opts) {
      this.queue.add(function(self) {
        self.url += '/' + resource + '/' + parseParams(opts || {});
      });

      return this;
    };
  };

  $.jribbble.shots = function(undefined, opts) {
    var shotArgsNegotiated = negotiateArgs([].slice.call(arguments)) || {};
    var shotsParams = opts || {};

    // Because most shot subresources; likes, projects, buckets, etc. all do
    // pretty much the same thing, we can avoid repeating code by using
    // currying. For each subresource we call this function and pass it the name
    // of the resource, it returns jribbble API method for that resource.
    // Yay programming!
    var shotSubResource = function(resource) {
      return function(undefined, opts) {
        var negotiated = negotiateArgs([].slice.call(arguments)) || {};
        var params = opts || {};

        this.queue.add(function(self) {
          if (!self.shotId) {
            throw new Error(ERROR_MSGS.shotId(resource));
          }

          self.url += '/' + resource + '/';

          if (negotiated.resource) {
            self.url += negotiated.resource;
            delete negotiated.resource;
          }

          self.url += parseParams($.extend(negotiated, params));
        });

        return this;
      };
    };

    var Shots = function() {
      $.extend(this, jribbbleBase());

      this.url += '/shots/';

      this.queue.add(function(self) {
        if (shotArgsNegotiated.resource) {
          self.shotId = shotArgsNegotiated.resource;
          self.url += shotArgsNegotiated.resource;
          delete shotArgsNegotiated.resource;
        }

        self.url += parseParams($.extend(shotArgsNegotiated, shotsParams));
      });

      // Jribbble seems to need an async queue, because we need to run the
      // server request at the end of the chain, but we will never know how
      // long the chain is. This is a super hack way of "waiting" to make sure
      // the queue is stocked before we flush it.
      setTimeout(function() {
        this.queue.flush(this).get();
      }.bind(this));

      return this;
    };

    Shots.prototype.attachments = shotSubResource('attachments');
    Shots.prototype.buckets = shotSubResource('buckets');
    Shots.prototype.likes = shotSubResource('likes');
    Shots.prototype.projects = shotSubResource('projects');
    Shots.prototype.rebounds = shotSubResource('rebounds');

    // Comments is a slightly different subresource because it has it's own
    // likes subresource. Comments shares a number of things with the other
    // shot subresources, but I haven't been able to figure out how to use
    // the shotSubResource currying function here to reduce repitition because
    // of the likes subresource.
    // I think I could get that to work if I created comments as a new Object
    // like comments = new Comments(). Then likes could be added to the
    // prototype of the Comments instance?
    // TODO: Figure that out.
    Shots.prototype.comments = function(undefined, opts) {
      var commentsArgsNegotiated = negotiateArgs([].slice.call(arguments)) || {};
      var commentsParams = opts || {};

      this.queue.add(function(self) {
        if (!self.shotId) {
          throw new Error(ERROR_MSGS.shotId('comments'));
        }

        self.url += '/comments/';

        // If we're looking for a specific comment by its ID.
        if (commentsArgsNegotiated.resource) {
          self.commentId = commentsArgsNegotiated.resource;
          self.url += commentsArgsNegotiated.resource + '/';
          delete commentsArgsNegotiated.resource;
        }

        self.url += parseParams($.extend(commentsArgsNegotiated, commentsParams));
      });

      this.likes = function(opts) {
        var params = opts || {};

        this.queue.add(function(self) {
          if (!self.commentId) {
            throw new Error(ERROR_MSGS.commentLikes);
          }

          self.url += 'likes/' + parseParams(params);
        });

        return this;
      };

      return this;
    };

    return new Shots();
  };

  $.jribbble.teams = function(id) {
    var resource = 'teams';
    var resourceId = checkId(id, resource);
    var Teams = resourceWithoutOpts.call(this, resource);

    Teams.prototype = createSubResources.call(this, [
      'members',
      'shots'
    ]);

    return new Teams(resourceId);
  };

  $.jribbble.users = function(id) {
    var resource = 'users';
    var resourceId = checkId(id, resource);
    var Users = resourceWithoutOpts.call(this, resource);

    Users.prototype = createSubResources.call(this, [
      'buckets',
      'followers',
      'following',
      'likes',
      'projects',
      'shots',
      'teams'
    ]);

    Users.prototype.isFollowing = function(targetUser) {
      this.queue.add(function(self) {
        self.url += '/following/' + targetUser;
      });

      return this;
    };

    return new Users(resourceId);
  };

  $.jribbble.buckets = function(id) {
    var resource = 'buckets';
    var resourceId = checkId(id, resource);
    var Buckets = resourceWithoutOpts.call(this, resource);
    Buckets.prototype = createSubResources.call(this, ['shots']);

    return new Buckets(resourceId);
  };

  $.jribbble.projects = function(id) {
    var resource = 'projects';
    var resourceId = checkId(id, resource);
    var Projects = resourceWithoutOpts.call(this, resource);
    Projects.prototype = createSubResources.call(this, ['shots']);

    return new Projects(resourceId);
  };

  $.jribbble.setToken = function(token) {
    ACCESS_TOKEN = token;
    return this;
  };
})(jQuery, window , document);
