/*! Sauron - v1.1.0 - 2013-01-07
* https://github.com/Ensighten/Sauron
* Copyright (c) 2013 Ensighten; Licensed MIT */

define(function () {
return (function () {
  var MiddleEarth = {},
      Sauron = {},
      console = window.console || {'log': function () {}};

  /**
   * Found this goodie on wiki: (Palantir == Seeing Stone)
   * http://en.wikipedia.org/wiki/Palant%C3%ADr
   */
  function Palantir() {
    this.stack = [];
  }
  function popStack() {
    return this.stack.pop();
  }
  var PalantirProto = Palantir.prototype = {
    /**
     * Retrieval function for the current channel
     * @param {Boolean} raw If true, prefixing will be skipped
     * @returns {String}
     */
    'channel': function (raw) {
      var stack = this.stack,
          prefix = this._prefix,
          controller = this._controller,
          model = this._model,
          channel = stack[stack.length - 1] || '';

      // If we don't want a raw channel
      if (!raw) {
        // If there is a prefix, use it
        if (prefix !== undefined) {
          channel = prefix + '/' + channel;
        }

        // If there is a controller, prefix the channel
        if (controller !== undefined) {
          channel = 'controller/' + controller + '/' + channel;
        } else if (model !== undefined) {
        // Otherwise, if there is a model, prefix the channel
          channel = 'model/' + model + '/' + channel;
        }
      }

      return channel;
    },
    /**
     * Maintenance functions for the stack of channels
     * @returns {String}
     */
    'pushStack': function (channel) {
      this.stack.push(channel);
      return this;
    },
    'popStack': popStack,
    'end': function () {
      var that = this.clone();
      popStack.call(that);
      return that;
    },
    'of': function (subchannel) {
      var that = this.clone(),
          lastChannel = that.channel(true),
          channel = lastChannel + '/' + subchannel;

      that.pushStack(channel);

      that.log('CHANNEL EDITED: ', that.channel());

      return that;
    },
    /**
     * Subscribing function for event listeners
     * @param {String} [subchannel] Subchannel to listen to
     * @param {Function} [fn] Function to subscribe with
     * @returns {this.clone}
     */
    'on': function (subchannel, fn) {
      var that = this.clone();

      // Move the track to do an 'on' action
      if (that.method !== 'on') {
        that.method = 'on';
        that.log('METHOD CHANGED TO: on');
      }

      // If there are are arguments
      if (arguments.length > 0) {
        // If there is only one argument and subchannel is a function, promote the subchannel to fn
        if (arguments.length === 1 && typeof subchannel === 'function') {
          fn = subchannel;
          subchannel = null;
        }

        // If there is a subchannel, add it to the current channel
        if (subchannel || subchannel === 0) {
          that = that.of(subchannel);
        }

        // If there is a function
        if (fn) {
          // Get the proper channel
          var channelName = that.channel(),
              channel = MiddleEarth[channelName];

          that.log('FUNCTION ADDED TO: ', channelName);

          // If the channel does not exist, create it
          if (channel === undefined) {
            channel = [];
            MiddleEarth[channelName] = channel;
          }

          // Save the function to this and context to the function
          that.fn = fn;
          fn.SAURON_CONTEXT = that;

          // Add the function to the channel
          channel.push(fn);

          /* let the clone be returned so callers can easily unsubscribe their events
          // This is a terminal event so return Sauron
          return Sauron;
          */
        }
      }

      // Return a clone
      return that;
    },
    /**
     * Unsubscribing function for event listeners
     * @param {String} [subchannel] Subchannel to unsubscribe from to
     * @param {Function} [fn] Function to remove subscription on
     * @returns {this.clone}
     */
    'off': function (subchannel, fn) {
      var that = this.clone();

      // Move the track to do an 'off' action
      if (that.method !== 'off') {
        that.method = 'off';
        that.log('METHOD CHANGED TO: off');
      }
      // If there are are arguments or there is a function
      fn = fn || that.fn;

      if (arguments.length > 0 || fn) {
        // If there is only one argument and subchannel is a function, promote the subchannel to fn
        if (arguments.length === 1 && typeof subchannel === 'function') {
          fn = subchannel;
          subchannel = null;
        }

        // If there is a subchannel, add it to the current channel
        if (subchannel || subchannel === 0) {
          that = that.of(subchannel);
        }

        // If there is a function
        var channelName = that.channel();
        if (fn) {
          // Get the proper channel
          var channel = MiddleEarth[channelName] || [],
              i = channel.length;

          that.log('REMOVING FUNCTION FROM: ', channelName);

          // Loop through the subscribers
          while (i--) {
            // If an functions match, remove them
            if (channel[i] === fn) {
              channel.splice(i, 1);
            }
          }

          // This is a terminal event so return Sauron
          return Sauron;
        } else {
        // Otherwise, unbind all items from the channel
          MiddleEarth[channelName] = [];
        }
      }

      // Return a clone
      return that;
    },
    /**
     * Voice/emit command for Sauron
     * @param {String|null} subchannel Subchannel to call on. If it is falsy, it will be skipped
     * @param {Mixed} [param] Parameter to voice to the channel. There can be infinite of these
     * @returns {Sauron}
     */
    'voice': function (subchannel/*, param, ... */) {
      var that = this.clone();

      // If there is a subchannel, use it
      if (subchannel || subchannel === 0) {
        that = that.of(subchannel);
      }

      // Collect the data and channel
      var args = [].slice.call(arguments, 1),
          channelName = that.channel(),
      // Capture the subscribers in case of self-removal (e.g. once)
          channel = (MiddleEarth[channelName] || []).slice(),
          subscriber,
          i = 0,
          len = channel.length;

      that.log('EXECUTING FUNCTIONS IN: ', channelName);

      // Loop through the subscribers
      for (; i < len; i++) {
        subscriber = channel[i];

        // Call the function within its original context
        subscriber.apply(subscriber.SAURON_CONTEXT, args);
      }

      // This is a terminal event so return Sauron
      return Sauron;
    },
    /**
     * Returns a cloned copy of this
     * @returns {this.clone}
     */
    'clone': function () {
      var that = this,
          retObj = new Palantir(),
          key;

      for (key in that) {
        if (that.hasOwnProperty(key)) {
          retObj[key] = that[key];
        }
      }

      // Special treatment for the stack
      retObj.stack = [].slice.call(that.stack);

      // Return the modified item
      return retObj;
    },
    /**
     * Sugar subscribe function that listens to an event exactly once
     * @param {String} [subchannel] Subchannel to listen to
     * @param {Function} [fn] Function to subscribe with
     * @returns {this.clone}
     */
    'once': function (subchannel, fn) {
      var that = this.clone();

      // Move the track to do an 'on' action
      that.method = 'once';

      // If there are arguments
      if (arguments.length > 0) {
        // If there is only one argument and subchannel is a function, promote the subchannel to fn
        if (arguments.length === 1 && typeof subchannel === 'function') {
          fn = subchannel;
          subchannel = null;
        }

        // If there is no function, throw an error
        if (typeof fn !== 'function') {
          throw new Error('Sauron.once expected a function, received: ' + fn.toString);
        }

        // Upcast the function for subscription
        var subFn = function () {
          // Unsubcribe from this
          this.off();

          // Call the function in this context
          var args = [].slice.call(arguments);
          return fn.apply(this, args);
        };

        // Call .on and return
        return that.on(subchannel, subFn);
      }

      // Return a clone
      return that;
    },

    // New hotness for creation/deletion
    'make': function () {
      var that = this.clone();

      that.log('PREFIX UPDATED TO: make');
      that._prefix = 'make';

      // If there are arguments, perform the normal action
      if (arguments.length > 0) {
        var args = [].slice.call(arguments),
            method = that.method || 'voice';

        return that[method].apply(that, args);
      } else {
      // Otherwise, return that
        return that;
      }
    },
    'destroy': function () {
      var that = this.clone();

      that.log('PREFIX UPDATED TO: destroy');
      that._prefix = 'destroy';

      // If there are arguments, perform the normal action
      if (arguments.length > 0) {
        var args = [].slice.call(arguments),
            method = that.method || 'voice';

        return that[method].apply(that, args);
      } else {
      // Otherwise, return that
        return that;
      }
    },

    // Controller methods
    /**
     * Fluent method for calling out a controller
     * @param {String} controller Name of the controller to invoke
     * @param {Mixed} * If there are any arguments, they will be passed to (on, off, once, voice) for invocation
     * @returns {Mixed} If there are more arguments than controller, the (on, off, once, voice) response will be returned. Otherwise, this.clone
     */
    'controller': function (controller) {
      var that = this.clone();

      that._controller = controller;

      // this.log('CONTROLLER UPDATED TO:', controller);
      that.log('CHANNEL UPDATED TO:', that.channel());

      // If require is present
      if (require) {
        var controllerUrl = require.getContext().config.paths._controllerDir || '',
            url = controllerUrl + controller;

        // If the controller has not yet been loaded by requirejs, notify
        if (!require.has(url)) {
          console.log(controller + ' has not been loaded by requirejs');
        }
      }

      if (arguments.length > 1) {
        var args = [].slice.call(arguments, 1),
            method = that.method || 'voice';
        args.unshift(null);
        return that[method].apply(that, args);
      } else {
      // Otherwise, return a clone
        return that;
      }
    },
    'createController': function (controller) {
      var that = this.clone();

      that = that.make();
      that = that.controller(controller);

      var args = [].slice.call(arguments, 1),
          method = that.method || 'voice';
      args.unshift(null);
      return that[method].apply(that, args);
    },
    'start': execFn('start'),
    'stop': execFn('stop'),

    // Model methods
    'model': function (model) {
      var that = this.clone();

      that._model = model;

      // this.log('MODEL UPDATED TO:', model);
      that.log('CHANNEL UPDATED TO:', that.channel());

      // If require is present
      if (require) {
        var modelUrl = require.getContext().config.paths._modelDir || '',
            url = modelUrl + model;

        // If the controller has not yet been loaded by requirejs, notify
        if (!require.has(url)) {
          console.log(model + ' has not been loaded by requirejs');
        }
      }

      if (arguments.length > 1) {
        var args = [].slice.call(arguments, 1),
            method = that.method || 'voice';
        args.unshift(null);
        return that[method].apply(that, args);
      } else {
      // Otherwise, return a clone
        return that;
      }
    },
    'createModel': function (model) {
      var that = this.clone();

      that = that.make();
      that = that.model(model);

      var args = [].slice.call(arguments, 1),
          method = that.method || 'voice';
      args.unshift(null);
      return that[method].apply(that, args);
    },
    'create': execFn('create'),
    'retrieve': execFn('retrieve'),
    'update': execFn('update'),
    'delete': execFn('delete'),
    'createEvent': execFn('createEvent'),
    'retrieveEvent': execFn('retrieveEvent'),
    'updateEvent': execFn('updateEvent'),
    'deleteEvent': execFn('deleteEvent'),

    /**
     * Helper function for error first callbacks. If an error occurs, we will log it and not call the function.
     * @param {Function} fn Function to remove error for
     * @returns {Function}
     */
    'noError': function (fn) {
      return function (err) {
        // If an error occurred, log it and don't do anything else
        if (err) { return console.error(err); }

        // Otherwise, callback with the remaining arguments
        var args = [].slice.call(arguments, 1);
        fn.apply(this, args);
      };
    },

    // Debug functions
    /**
     * Setter function for debugging
     * @param {Boolean} debug If true, turn debugger on. Otherwise, leave it off
     * @returns {this}
     */
    'debug': function (debug) {
      var that = this.clone();
      that._debug = debug;
      return that;
    },
    /**
     * Debug logger for this object
     * @returns {this}
     */
    'log': function () {
      if (this._debug === true || Sauron._debug === true) {
        console.log.apply(console, arguments);
      }
      return this;
    }
  };

  function execFn(subchannel) {
    return function () {
      var that = this.clone();

      // Add subchannel to the channel
      that = that.of(subchannel);

      // If there are arguments, perform the normal action
      if (arguments.length > 0) {
        var args = [].slice.call(arguments),
            method = that.method || 'voice';

        // If the method is voice, unshift an empty subchannel
        args.unshift(null);

        return that[method].apply(that, args);
      } else {
      // Otherwise, return a clone
        return that;
      }
    };
  }

  // Copy over all of the items in the Palantir prototype to Sauron such that each one is run on a fresh Palantir
  for (var key in PalantirProto) {
    if (PalantirProto.hasOwnProperty(key)) {
      (function (fn) {
        Sauron[key] = function () {
          var args = [].slice.call(arguments);
          return fn.apply(new Palantir(), args);
        };
      }(PalantirProto[key]));
    }
  }

  return Sauron;
}());
});