var _ = require('lodash'),
  utils = require('../utils'),
  isTag = utils.isTag,
  domEach = utils.domEach,
  hasOwn = Object.prototype.hasOwnProperty,
  camelCase = utils.camelCase,
  cssCase = utils.cssCase,
  rspace = /\s+/,
  dataAttrPrefix = 'data-',

  // Lookup table for coercing string data-* attributes to their corresponding
  // JavaScript primitives
  primitives = {
    null: null,
    true: true,
    false: false
  },

  // Attributes that are booleans
  rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,
  // Matches strings that look like JSON objects or arrays
  rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/;


var getAttr = function(elem, name) {
  if (!elem || !isTag(elem)) return;

  if (!elem.attribs) {
    elem.attribs = {};
  }

  // Return the entire attribs object if no attribute specified
  if (!name) {
    return elem.attribs;
  }

  if (hasOwn.call(elem.attribs, name)) {
    // Get the (decoded) attribute
    return rboolean.test(name) ? name : elem.attribs[name];
  }
};

var setAttr = function(el, name, value) {

  if (value === null) {
    removeAttribute(el, name);
  } else {
    el.attribs[name] = value+'';
  }
};

exports.attr = function(name, value) {
  // Set the value (with attr map support)
  if (typeof name === 'object' || value !== undefined) {
    if (typeof value === 'function') {
      return domEach(this, function(i, el) {
        setAttr(el, name, value.call(el, i, el.attribs[name]));
      });
    }
    return domEach(this, function(i, el) {
      if (!isTag(el)) return;

      if (typeof name === 'object') {
        _.each(name, function(name, key) {
          el.attribs[key] = name+'';
        });
      } else {
        setAttr(el, name, value);
      }
    });
  }

  return getAttr(this[0], name);
};

var setData = function(el, name, value) {
  if (typeof name === 'object') return _.extend(el.data, name);
  if (typeof name === 'string' && value !== undefined) {
    el.data[name] = value;
  } else if (typeof name === 'object') {
    _.exend(el.data, name);
  }
};

// Read the specified attribute from the equivalent HTML5 `data-*` attribute,
// and (if present) cache the value in the node's internal data store. If no
// attribute name is specified, read *all* HTML5 `data-*` attributes in this
// manner.
var readData = function(el, name) {
  var readAll = arguments.length === 1;
  var domNames, domName, jsNames, jsName, value, idx, length;

  if (readAll) {
    domNames = Object.keys(el.attribs).filter(function(attrName) {
      return attrName.slice(0, dataAttrPrefix.length) === dataAttrPrefix;
    });
    jsNames = domNames.map(function(domName) {
      return camelCase(domName.slice(dataAttrPrefix.length));
    });
  } else {
    domNames = [dataAttrPrefix + cssCase(name)];
    jsNames = [name];
  }

  for (idx = 0, length = domNames.length; idx < length; ++idx) {
    domName = domNames[idx];
    jsName = jsNames[idx];
    if (hasOwn.call(el.attribs, domName)) {
      value = el.attribs[domName];

      if (hasOwn.call(primitives, value)) {
        value = primitives[value];
      } else if (value === String(Number(value))) {
        value = Number(value);
      } else if (rbrace.test(value)) {
        try {
          value = JSON.parse(value);
        } catch(e){ }
      }

      el.data[jsName] = value;
    }
  }

  return readAll ? el.data : value;
};

exports.data = function(name, value) {
  var elem = this[0];

  if (!elem || !isTag(elem)) return;

  if (!elem.data) {
    elem.data = {};
  }

  // Return the entire data object if no data specified
  if (!name) {
    return readData(elem);
  }

  // Set the value (with attr map support)
  if (typeof name === 'object' || value !== undefined) {
    domEach(this, function(i, el) {
      setData(el, name, value);
    });
    return this;
  } else if (hasOwn.call(elem.data, name)) {
    return elem.data[name];
  }

  return readData(elem, name);
};

/**
 * Get the value of an element
 */

exports.val = function(value) {
  var querying = arguments.length === 0,
      element = this[0];

  if(!element) return;

  switch (element.name) {
    case 'textarea':
      return this.text(value);
    case 'input':
      switch (this.attr('type')) {
        case 'radio':
          if (querying) {
            return this.attr('value');
          } else {
            this.attr('value', value);
            return this;
          }
          break;
        default:
          return this.attr('value', value);
      }
      return;
    case 'select':
      var option = this.find('option:selected'),
          returnValue;
      if (option === undefined) return undefined;
      if (!querying) {
        if (!this.attr().hasOwnProperty('multiple') && typeof value == 'object') {
          return this;
        }
        if (typeof value != 'object') {
          value = [value];
        }
        this.find('option').removeAttr('selected');
        for (var i = 0; i < value.length; i++) {
          this.find('option[value="' + value[i] + '"]').attr('selected', '');
        }
        return this;
      }
      returnValue = option.attr('value');
      if (this.attr().hasOwnProperty('multiple')) {
        returnValue = [];
        domEach(option, function(i, el) {
          returnValue.push(el.attribs.value);
        });
      }
      return returnValue;
    case 'option':
      if (!querying) {
        this.attr('value', value);
        return this;
      }
      return this.attr('value');
  }
};

/**
 * Remove an attribute
 */

var removeAttribute = function(elem, name) {
  if (!elem.attribs || !hasOwn.call(elem.attribs, name))
    return;

  delete elem.attribs[name];
};


exports.removeAttr = function(name) {
  domEach(this, function(i, elem) {
    removeAttribute(elem, name);
  });

  return this;
};

exports.hasClass = function(className) {
  return _.any(this, function(elem) {
    var attrs = elem.attribs,
        clazz = attrs && attrs['class'],
        idx = -1,
        end;

    if (clazz) {
      while ((idx = clazz.indexOf(className, idx+1)) > -1) {
        end = idx + className.length;

        if ((idx === 0 || rspace.test(clazz[idx-1]))
            && (end === clazz.length || rspace.test(clazz[end]))) {
          return true;
        }
      }
    }
  });
};

exports.addClass = function(value) {
  // Support functions
  if (typeof value === 'function') {
    return domEach(this, function(i, el) {
      var className = el.attribs['class'] || '';
      exports.addClass.call([el], value.call(el, i, className));
    });
  }

  // Return if no value or not a string or function
  if (!value || typeof value !== 'string') return this;

  var classNames = value.split(rspace),
      numElements = this.length;


  for (var i = 0; i < numElements; i++) {
    // If selected element isn't a tag, move on
    if (!isTag(this[i])) continue;

    // If we don't already have classes
    var className = getAttr(this[i], 'class'),
        numClasses,
        setClass;

    if (!className) {
      setAttr(this[i], 'class', classNames.join(' ').trim());
    } else {
      setClass = ' ' + className + ' ';
      numClasses = classNames.length;

      // Check if class already exists
      for (var j = 0; j < numClasses; j++) {
        var appendClass = classNames[j] + ' ';
        if (setClass.indexOf(' ' + appendClass) < 0)
          setClass += appendClass;
      }

      setAttr(this[i], 'class', setClass.trim());
    }
  }

  return this;
};

var splitClass = function(className) {
  return className ? className.trim().split(rspace) : [];
};

exports.removeClass = function(value) {
  var classes,
      numClasses,
      removeAll;

  // Handle if value is a function
  if (typeof value === 'function') {
    return domEach(this, function(i, el) {
      exports.removeClass.call(
        [el], value.call(el, i, el.attribs['class'] || '')
      );
    });
  }

  classes = splitClass(value);
  numClasses = classes.length;
  removeAll = arguments.length === 0;

  return domEach(this, function(i, el) {
    if (!isTag(el)) return;

    if (removeAll) {
      // Short circuit the remove all case as this is the nice one
      el.attribs.class = '';
    } else {
      var elClasses = splitClass(el.attribs.class),
          index,
          changed;

      for (var j = 0; j < numClasses; j++) {
        index = elClasses.indexOf(classes[j]);

        if (index >= 0) {
          elClasses.splice(index, 1);
          changed = true;

          // We have to do another pass to ensure that there are not duplicate
          // classes listed
          j--;
        }
      }
      if (changed) {
        el.attribs.class = elClasses.join(' ');
      }
    }
  });
};

exports.toggleClass = function(value, stateVal) {
  // Support functions
  if (typeof value === 'function') {
    return domEach(this, function(i, el) {
      exports.toggleClass.call(
        [el],
        value.call(el, i, el.attribs['class'] || '', stateVal),
        stateVal
      );
    });
  }

  // Return if no value or not a string or function
  if (!value || typeof value !== 'string') return this;

  var classNames = value.split(rspace),
    numClasses = classNames.length,
    state = typeof stateVal === 'boolean' ? stateVal ? 1 : -1 : 0,
    numElements = this.length,
    elementClasses,
    index;

  for (var i = 0; i < numElements; i++) {
    // If selected element isn't a tag, move on
    if (!isTag(this[i])) continue;

    elementClasses = splitClass(this[i].attribs.class);

    // Check if class already exists
    for (var j = 0; j < numClasses; j++) {
      // Check if the class name is currently defined
      index = elementClasses.indexOf(classNames[j]);

      // Add if stateValue === true or we are toggling and there is no value
      if (state >= 0 && index < 0) {
        elementClasses.push(classNames[j]);
      } else if (state <= 0 && index >= 0) {
        // Otherwise remove but only if the item exists
        elementClasses.splice(index, 1);
      }
    }

    this[i].attribs.class = elementClasses.join(' ');
  }

  return this;
};

exports.is = function (selector) {
  if (selector) {
    return this.filter(selector).length > 0;
  }
  return false;
};

