(function(factory){
  if (typeof define === 'function' && define.amd) {
    define(['jquery', 'jquery-qubit'], factory);
  } else if (typeof exports === 'object') {
    factory(require('jquery'), require('jquery-qubit'));
  } else {
    factory(jQuery, jQuery.fn.qubit);
  }
}(function($, qubit) {
  $.fn.bonsai = function(options) {
    var args = arguments;
    return this.each(function() {
      var bonsai = $(this).data('bonsai');
      if (!bonsai) {
        bonsai = new Bonsai(this, options);
        $(this).data('bonsai', bonsai);
      }
      if (typeof options == 'string') {
        var method = options;
        return bonsai[method].apply(bonsai, [].slice.call(args, 1));
      }
    });
  };
  $.bonsai = {};
  $.bonsai.defaults = {
    expandAll: false, // expand all items
    expand: null, // optional function to expand an item
    collapse: null, // optional function to collapse an item
    addExpandAll: false, // add a link to expand all items
    addSelectAll: false, // add a link to select all checkboxes
    selectAllExclude: null, // a filter selector or function for selectAll
    idAttribute: 'id', // which attribute of the list items to use as an id

    // createInputs: create checkboxes or radio buttons for each list item
    // by setting createInputs to "checkbox" or "radio".
    //
    // The name and value for the inputs can be declared in the
    // markup using `data-name` and `data-value`.
    //
    // The name is inherited from parent items if not specified.
    //
    // Checked state can be indicated using `data-checked`.
    createInputs: false,
    // checkboxes: run qubit(this.options) on the root node (requires jquery.qubit)
    checkboxes: false,
    // handleDuplicateCheckboxes: update any other checkboxes that
    // have the same value
    handleDuplicateCheckboxes: false,
    // createRadioButtons: creates radio buttons for each list item.
    //
    // The name and value for the checkboxes can be declared in the
    // markup using `data-name` and `data-value`.
    //
    // The name is inherited from parent items if not specified.
    //
    // Checked state can be indicated using `data-checked`.
    createRadioButtons: false
  };
  var Bonsai = function(el, options) {
    var self = this;
    options = options || {};
    this.options = $.extend({}, $.bonsai.defaults, options);
    this.el = $(el).addClass('bonsai').data('bonsai', this);

    // store the scope in the options for child nodes
    if (!this.options.scope) {
      this.options.scope = this.el;
    }
    this.update();
    if (this.isRootNode()) {
      if (this.options.createCheckboxes) this.createInputs = 'checkbox';
      if (this.options.handleDuplicateCheckboxes) this.handleDuplicateCheckboxes();
      if (this.options.checkboxes) this.el.qubit(this.options);
      if (this.options.addExpandAll) this.addExpandAllLink();
      if (this.options.addSelectAll) this.addSelectAllLink();
      this.el.on('click', '.thumb', function(ev) {
        self.toggle($(ev.currentTarget).closest('li'));
      });
    }
    if (this.options.expandAll) this.expandAll();
  };
  Bonsai.prototype = {
    isRootNode: function() {
      return this.options.scope == this.el;
    },
    listItem: function(id) {
      if (typeof id === 'object') return $(id);
      return this.el.find('[' + this.options.idAttribute + '="' + id + '"]');
    },
    toggle: function(listItem) {
      if (!$(listItem).hasClass('expanded')) {
        return this.expand(listItem);
      }
      else {
        return this.collapse(listItem);
      }
    },
    expand: function(listItem) {
      return this.setExpanded(listItem, true);
    },
    collapse: function(listItem) {
      return this.setExpanded(listItem, false);
    },
    setExpanded: function(listItem, expanded) {
      var $li = this.listItem(listItem);
      if ($li.length > 1) {
        var self = this;
        $li.each(function() {
          self.setExpanded(this, expanded);
        });
        return;
      }
      if (expanded) {
        if (!$li.data('subList')) return;
        $li = $($li).addClass('expanded').removeClass('collapsed');
        $($li.data('subList')).css('height', 'auto');
      }
      else {
        $li = $($li).addClass('collapsed')
          .removeClass('expanded');
        $($li.data('subList')).height(0);
      }
      return $li;
    },
    expandAll: function() {
      this.expand(this.el.find('li'));
    },
    collapseAll: function() {
      this.collapse(this.el.find('li'));
    },
    expandTo: function(listItem) {
      var self = this;
      var $li = this.listItem(listItem);
      $li.parents('li').each(function () {
        self.expand($(this));
      });
      return $li;
    },
    update: function() {
      var self = this;
      // look for a nested list (if any)
      this.el.children().each(function() {
        var item = $(this);
        if (self.options.createInputs) self.insertInput(item);

        // insert a thumb if it doesn't already exist
        if (item.children().filter('.thumb').length == 0) {
          var thumb = $('<div class="thumb"></div>');
          item.prepend(thumb);
        }
        var subLists = item.children().filter('ol, ul');
        item.toggleClass('has-children', subLists.find('li').length > 0);
        // if there is a child list
        subLists.each(function() {
          // that's not empty
          if ($('li', this).length == 0) {
            return;
          }
          // then this el has children
          item.data('subList', this);
          // collapse the nested list
          if (item.hasClass('expanded')) {
            self.expand(item);
          }
          else {
            self.collapse(item);
          }
          // handle any deeper nested lists
          var exists = !!$(this).data('bonsai');
          $(this).bonsai(exists ? 'update' : self.options);
        });
      });

      this.expand = this.options.expand || this.expand;
      this.collapse = this.options.collapse || this.collapse;
    },
    serialize: function() {
      var idAttr = this.options.idAttribute;
      return this.el.find('li').toArray().reduce(function(acc, li) {
        var $li = $(li);
        var id = $li.attr(idAttr);
        // only items with IDs can be serialized
        if (id) {
          var state = $li.hasClass('expanded')
              ? 'expanded'
              : ($li.hasClass('collapsed') ? 'collapsed' : null);
          if (state) acc[$li.hasClass('expanded') ? 'expanded' : 'collapsed'].push(id);
        }
        return acc;
      }, {expanded: [], collapsed: [], version: 2});
    },
    restore: function(state) {
      var self = this;
      if (state.version > 1) {
        state.expanded.map(this.expand.bind(this));
        state.collapsed.map(this.collapse.bind(this));
      }
      else {
        Object.keys(state).forEach(function(id) {
          self.setExpanded(id, state[id] === 'expanded');
        });
      }
    },
    insertInput: function(listItem) {
      var type = this.options.createInputs;
      if (listItem.find('> input[type=' + type + ']').length) return;
      var id = this.inputIdFor(listItem);
      var checkbox = $('<input type="' + type + '" name="'
        + this.inputNameFor(listItem) + '" id="' + id + '" /> '
      );
      var children = listItem.children();
      // get the first text node for the label
      var text = listItem.contents().filter(function() {
        return this.nodeType == 3;
      }).first();
      checkbox.val(listItem.data('value'));
      checkbox.prop('checked', listItem.data('checked'))
      children.detach();
      listItem.append(checkbox)
        .append(
          $('<label for="' + id + '">').append(text.length > 0 ? text : children.first())
        )
        .append(text.length > 0 ? children : children.slice(1));
    },
    checkboxPrefix: 'bonsai-checkbox-',
    inputIdFor: function(listItem) {
      var id = $(listItem).data('id');
      while (!id || ($('#' + id).length > 0)) {
        id = this.checkboxPrefix + Bonsai.uniqueId++;
      }
      return id;
    },
    inputNameFor: function(listItem) {
      return listItem.data('name')
        || listItem.parents().filter('[data-name]').data('name');
    },
    handleDuplicateCheckboxes: function() {
      var self = this;
      self.el.on('change', 'input[type=checkbox]', function(ev) {
        var checkbox = $(ev.target);
        if (!checkbox.val()) return;
        // select all duplicate checkboxes that need to be updated
        var selector = 'input[type=checkbox]'
          + '[value="' + checkbox.val() + '"]'
          + (checkbox.attr('name') ? '[name="' + checkbox.attr('name') + '"]' : '')
          + (checkbox.prop('checked') ? ':not(:checked)' : ':checked');
        self.el.find(selector).prop({
          checked: checkbox.prop('checked'),
          indeterminate: checkbox.prop('indeterminate')
        }).trigger('change');
      });
    },
    addExpandAllLink: function() {
      var self = this;
      $('<div class="expand-all">')
        .append(
          $('<a class="all">Expand all</a>').on('click', function() {
            self.expandAll();
          })
        )
        .append('<i class="separator"></i>')
        .append(
          $('<a class="none">Collapse all</a>').on('click', function() {
            self.collapseAll();
          })
        )
        .insertBefore(this.el);
    },
    addSelectAllLink: function() {
      var scope = this.options.scope;
      var self = this;
      function getCheckboxes() {
        // return all checkboxes that are not in hidden list items
        return scope.find('li')
          .filter(self.options.selectAllExclude || function() {
            return $(this).css('display') != 'none';
          })
          .find('> input[type=checkbox]');
      }
      $('<div class="check-all">')
        .append($('<a class="all">Select all</a>')
          .css('cursor', 'pointer')
          .on('click', function() {
            getCheckboxes().prop({
              checked: true,
              indeterminate: false
            });
          }))
        .append('<i class="separator"></i>')
        .append($('<a class="none">Select none</a>')
          .css('cursor', 'pointer')
          .on('click', function() {
            getCheckboxes().prop({
              checked: false,
              indeterminate: false
            });
          })
      )
        .insertAfter(this.el);
    },
    setCheckedValues: function(values) {
      var all = this.options.scope.find('input[type=checkbox]');
      $.each(values, function(key, value) {
        all.filter('[value="' + value + '"]')
          .prop('checked', true)
          .trigger('change');
      });
    }
  };
  $.extend(Bonsai, {
    uniqueId: 0
  });
}));
