/*!
 * Colcade v0.2.0
 * Lightweight masonry layout
 * by David DeSandro
 * MIT license
 */

/*jshint browser: true, undef: true, unused: true */

(function (window, factory) {
  // universal module definition
  /*jshint strict: false */
  /*global define: false, module: false */
  if (typeof define == "function" && define.amd) {
    // AMD
    define(factory);
  } else if (typeof module == "object" && module.exports) {
    // CommonJS
    module.exports = factory();
  } else {
    // browser global
    window.Colcade = factory();
  }
})(window, function factory() {
  // -------------------------- Colcade -------------------------- //

  function Colcade(element, options) {
    element = getQueryElement(element);

    // do not initialize twice on same element
    if (element && element.colcadeGUID) {
      var instance = instances[element.colcadeGUID];
      instance.option(options);
      return instance;
    }

    this.element = element;
    // options
    this.options = {};
    this.option(options);
    // kick things off
    this.create();
  }

  var proto = Colcade.prototype;

  proto.option = function (options) {
    this.options = extend(this.options, options);
  };

  // globally unique identifiers
  var GUID = 0;
  // internal store of all Colcade intances
  var instances = {};

  proto.create = function () {
    this.errorCheck();
    // add guid for Colcade.data
    var guid = (this.guid = ++GUID);
    this.element.colcadeGUID = guid;
    instances[guid] = this; // associate via id
    // update initial properties & layout
    this.reload();
    // events
    this._windowResizeHandler = this.onWindowResize.bind(this);
    this._loadHandler = this.onLoad.bind(this);
    window.addEventListener("resize", this._windowResizeHandler);
    this.element.addEventListener("load", this._loadHandler, true);
  };

  proto.errorCheck = function () {
    var errors = [];
    if (!this.element) {
      errors.push("Bad element: " + this.element);
    }
    if (!this.options.columns) {
      errors.push("columns option required: " + this.options.columns);
    }
    if (!this.options.items) {
      errors.push("items option required: " + this.options.items);
    }

    if (errors.length) {
      throw new Error("[Colcade error] " + errors.join(". "));
    }
  };

  // update properties and do layout
  proto.reload = function () {
    this.updateColumns();
    this.updateItems();
    this.layout();
  };

  proto.updateColumns = function () {
    this.columns = querySelect(this.options.columns, this.element);
  };

  proto.updateItems = function () {
    this.items = querySelect(this.options.items, this.element);
  };

  proto.getActiveColumns = function () {
    return this.columns.filter(function (column) {
      var style = getComputedStyle(column);
      return style.display != "none";
    });
  };

  // ----- layout ----- //

  // public, updates activeColumns
  proto.layout = function () {
    this.activeColumns = this.getActiveColumns();
    this._layout();
  };

  // private, does not update activeColumns
  proto._layout = function () {
    // reset column heights
    this.columnHeights = this.activeColumns.map(function () {
      return 0;
    });
    // layout all items
    this.layoutItems(this.items);
  };

  proto.layoutItems = function (items) {
    items.forEach(this.layoutItem, this);
  };

  proto.layoutItem = function (item) {
    // layout item by appending to column
    var minHeight = Math.min.apply(Math, this.columnHeights);
    var index = this.columnHeights.indexOf(minHeight);
    this.activeColumns[index].appendChild(item);
    // at least 1px, if item hasn't loaded
    // Not exactly accurate, but it's cool
    this.columnHeights[index] += item.offsetHeight || 1;
  };

  // ----- adding items ----- //

  proto.append = function (elems) {
    var items = this.getQueryItems(elems);
    // add items to collection
    this.items = this.items.concat(items);
    // lay them out
    this.layoutItems(items);
  };

  proto.prepend = function (elems) {
    var items = this.getQueryItems(elems);
    // add items to collection
    this.items = items.concat(this.items);
    // lay out everything
    this._layout();
  };

  proto.getQueryItems = function (elems) {
    elems = makeArray(elems);
    var fragment = document.createDocumentFragment();
    elems.forEach(function (elem) {
      fragment.appendChild(elem);
    });
    return querySelect(this.options.items, fragment);
  };

  // ----- measure column height ----- //

  proto.measureColumnHeight = function (elem) {
    var boundingRect = this.element.getBoundingClientRect();
    this.activeColumns.forEach(function (column, i) {
      // if elem, measure only that column
      // if no elem, measure all columns
      if (!elem || column.contains(elem)) {
        var lastChildRect = column.lastElementChild.getBoundingClientRect();
        // not an exact calculation as it includes top border, and excludes item bottom margin
        this.columnHeights[i] = lastChildRect.bottom - boundingRect.top;
      }
    }, this);
  };

  // ----- events ----- //

  proto.onWindowResize = function () {
    clearTimeout(this.resizeTimeout);
    this.resizeTimeout = setTimeout(
      function () {
        this.onDebouncedResize();
      }.bind(this),
      100
    );
  };

  proto.onDebouncedResize = function () {
    var activeColumns = this.getActiveColumns();
    // check if columns changed
    var isSameLength = activeColumns.length == this.activeColumns.length;
    var isSameColumns = true;
    this.activeColumns.forEach(function (column, i) {
      isSameColumns = isSameColumns && column == activeColumns[i];
    });
    if (isSameLength && isSameColumns) {
      return;
    }
    // activeColumns changed
    this.activeColumns = activeColumns;
    this._layout();
  };

  proto.onLoad = function (event) {
    this.measureColumnHeight(event.target);
  };

  // ----- destroy ----- //

  proto.destroy = function () {
    // move items back to container
    this.items.forEach(function (item) {
      this.element.appendChild(item);
    }, this);
    // remove events
    window.removeEventListener("resize", this._windowResizeHandler);
    this.element.removeEventListener("load", this._loadHandler, true);
    // remove data
    delete this.element.colcadeGUID;
    delete instances[this.guid];
  };

  // -------------------------- HTML init -------------------------- //

  docReady(function () {
    var dataElems = querySelect("[data-colcade]");
    dataElems.forEach(htmlInit);
  });

  function htmlInit(elem) {
    // convert attribute "foo: bar, qux: baz" into object
    var attr = elem.getAttribute("data-colcade");
    var attrParts = attr.split(",");
    var options = {};
    attrParts.forEach(function (part) {
      var pair = part.split(":");
      var key = pair[0].trim();
      var value = pair[1].trim();
      options[key] = value;
    });

    new Colcade(elem, options);
  }

  Colcade.data = function (elem) {
    elem = getQueryElement(elem);
    var id = elem && elem.colcadeGUID;
    return id && instances[id];
  };

  // -------------------------- jQuery -------------------------- //

  Colcade.makeJQueryPlugin = function ($) {
    $ = $ || window.jQuery;
    if (!$) {
      return;
    }

    $.fn.colcade = function (arg0 /*, arg1 */) {
      // method call $().colcade( 'method', { options } )
      if (typeof arg0 == "string") {
        // shift arguments by 1
        var args = Array.prototype.slice.call(arguments, 1);
        return methodCall(this, arg0, args);
      }
      // just $().colcade({ options })
      plainCall(this, arg0);
      return this;
    };

    function methodCall($elems, methodName, args) {
      var returnValue;
      $elems.each(function (i, elem) {
        // get instance
        var colcade = $.data(elem, "colcade");
        if (!colcade) {
          return;
        }
        // apply method, get return value
        var value = colcade[methodName].apply(colcade, args);
        // set return value if value is returned, use only first value
        returnValue = returnValue === undefined ? value : returnValue;
      });
      return returnValue !== undefined ? returnValue : $elems;
    }

    function plainCall($elems, options) {
      $elems.each(function (i, elem) {
        var colcade = $.data(elem, "colcade");
        if (colcade) {
          // set options & init
          colcade.option(options);
          colcade.layout();
        } else {
          // initialize new instance
          colcade = new Colcade(elem, options);
          $.data(elem, "colcade", colcade);
        }
      });
    }
  };

  // try making plugin
  Colcade.makeJQueryPlugin();

  // -------------------------- utils -------------------------- //

  function extend(a, b) {
    for (var prop in b) {
      a[prop] = b[prop];
    }
    return a;
  }

  // turn element or nodeList into an array
  function makeArray(obj) {
    var ary = [];
    if (Array.isArray(obj)) {
      // use object if already an array
      ary = obj;
    } else if (obj && typeof obj.length == "number") {
      // convert nodeList to array
      for (var i = 0; i < obj.length; i++) {
        ary.push(obj[i]);
      }
    } else {
      // array of single index
      ary.push(obj);
    }
    return ary;
  }

  // get array of elements
  function querySelect(selector, elem) {
    elem = elem || document;
    var elems = elem.querySelectorAll(selector);
    return makeArray(elems);
  }

  function getQueryElement(elem) {
    if (typeof elem == "string") {
      elem = document.querySelector(elem);
    }
    return elem;
  }

  function docReady(onReady) {
    if (document.readyState == "complete") {
      onReady();
      return;
    }
    document.addEventListener("DOMContentLoaded", onReady);
  }

  // -------------------------- end -------------------------- //

  return Colcade;
});
