var $ = require('../static'),

  utils = require('../utils'),
  isTag = utils.isTag,
  domEach = utils.domEach,
  hasOwn = Object.prototype.hasOwnProperty,
  camelCase = utils.camelCase,
  cssCase = utils.cssCase,
  rspace = /\s+/,
  dataAttrPrefix = 'data-',
  _ = {
    forEach: require('lodash.foreach'),
    extend: require('lodash.assignin'),
    some: require('lodash.some')
  },

// 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];
}

// Mimic the DOM and return text content as value for `option's`
if (elem.name === 'option' && name === 'value') {
  return $.text(elem.children);
}

// Mimic DOM with default value for radios/checkboxes
if (elem.name === 'input' &&
    (elem.attribs.type === 'radio' || elem.attribs.type === 'checkbox') &&
    name === 'value') {
  return 'on';
}

};

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') {
      _.forEach(name, function(value, name) {
        setAttr(el, name, value);
      });
    } else {
      setAttr(el, name, value);
    }
  });
}

return getAttr(this[0], name);

};

var getProp = function (el, name) {

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

return el.hasOwnProperty(name)
    ? el[name]
    : rboolean.test(name)
        ? getAttr(el, name) !== undefined
        : getAttr(el, name);

};

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

el[name] = rboolean.test(name) ? !!value : value;

};

exports.prop = function (name, value) {

var i = 0,
    property;

if (typeof name === 'string' && value === undefined) {

  switch (name) {
    case 'style':
      property = this.css();

      _.forEach(property, function (v, p) {
        property[i++] = p;
      });

      property.length = i;

      break;
    case 'tagName':
    case 'nodeName':
      property = this[0].name.toUpperCase();
      break;
    default:
      property = getProp(this[0], name);
  }

  return property;
}

if (typeof name === 'object' || value !== undefined) {

  if (typeof value === 'function') {
    return domEach(this, function(i, el) {
      setProp(el, name, value.call(el, i, getProp(el, name)));
    });
  }

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

    if (typeof name === 'object') {

      _.forEach(name, function(val, name) {
        setProp(el, name, val);
      });

    } else {
      setProp(el, name, value);
    }
  });

}

};

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

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

if (typeof name === 'object') return _.extend(el.data, name);
if (typeof name === 'string' && value !== undefined) {
  el.data[name] = value;
} else if (typeof name === 'object') {
  _.extend(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(getAttr(el, '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 _.some(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;

};