(function (factory) {
if (typeof define === 'function' && define.amd) {
	//AMD
	define(['leaflet'], factory);
} else if (typeof module !== 'undefined') {
	// Node/CommonJS
	module.exports = factory(require('leaflet'));
} else {
	// Browser globals
	if (typeof window.L === 'undefined')
		throw 'Leaflet must be loaded first';
	factory(window.L);
}
})(function (L) {

L.Control.PanelLayers = L.Control.Layers.extend({

	includes: L.version[0]==='1' ? L.Evented.prototype : L.Mixin.Events,

	options: {
		compact: false,
		compactOffset: 0,
		collapsed: false,
		autoZIndex: true,
		collapsibleGroups: false,
		selectorGroup: false,			//select all layer of a group
		buildItem: null,				//function that return row item html node(or html string)
		title: '',						//title of panel
		className: '',					//additional class name for panel
		position: 'topright'
	},

	initialize: function (baseLayers, overlays, options) {
		L.setOptions(this, options);
		this._layers = [];
		this._groups = {};
		this._items = {};
		this._layersActives = [];
		this._lastZIndex = 0;
		this._handlingClick = false;

		this.className = 'leaflet-panel-layers';

		var i, n, isCollapsed;

		for (i in baseLayers) {
			if (baseLayers[i].group && baseLayers[i].layers) {
				isCollapsed = baseLayers[i].collapsed || false;
				for (n in baseLayers[i].layers)
					this._addLayer(baseLayers[i].layers[n], false, baseLayers[i].group, isCollapsed);
			}
			else
				this._addLayer(baseLayers[i], false);
		}

		for (i in overlays) {
			if (overlays[i].group && overlays[i].layers) {
				isCollapsed = overlays[i].collapsed || false;
				for (n in overlays[i].layers)
					this._addLayer(overlays[i].layers[n], true, overlays[i].group, isCollapsed);
			}
			else
				this._addLayer(overlays[i], true);
		}
	},

	onAdd: function (map) {

		var self = this;

		for (var i in this._layersActives) {
			map.addLayer(this._layersActives[i]);
		}

		L.Control.Layers.prototype.onAdd.call(this, map);

		this._map.on('resize', function(e) {
			self._updateHeight(e.newSize.y);
		});

		// update group checkboxes
		this._onInputClick();

		return this._container;
	},

	//TODO addBaseLayerGroup
	//TODO addOverlayGroup

	addBaseLayer: function (layer, name, group) {
		layer.name = name || layer.name || '';
		this._addLayer(layer, false, group);
		this._update();
		return this;
	},

	addOverlay: function (layer, name, group) {
		layer.name = name || layer.name || '';
		this._addLayer(layer, true, group);
		this._update();
		return this;
	},

	removeLayer: function (layerDef) {
		var layer = layerDef.hasOwnProperty('layer') ? this._layerFromDef(layerDef) : layerDef;

		this._map.removeLayer(layer);

		L.Control.Layers.prototype.removeLayer.call(this, layer);
		return this;
	},

	clearLayers: function () {
		for (var i = 0; i < this._layers.length; i++) {
			this.removeLayer(this._layers[i]);
		}
	},

	_layerFromDef: function (layerDef) {
		for (var i = 0; i < this._layers.length; i++) {
			var id = L.stamp(this._layers[i].layer);
			//TODO add more conditions to comparing definitions
			if (this._getLayer(id).name === layerDef.name)
				return this._getLayer(id).layer;
		}
	},

	_update: function () {
		this._groups = {};
		this._items = {};
		L.Control.Layers.prototype._update.call(this);
	},

	_getLayer: function (id) {
		for (var i = 0; i < this._layers.length; i++) {
			if (this._layers[i] && this._layers[i].id == id) {
				return this._layers[i];
			}
		}
	},

	_addLayer: function (layerDef, overlay, group, isCollapsed) {

		if(!layerDef.layer)
			throw new Error('layer not defined in item: '+(layerDef.name||''));

		if (!(layerDef.layer instanceof L.Class) &&
			(layerDef.layer.type && layerDef.layer.args)) {
			layerDef.layer = this._getPath(L, layerDef.layer.type).apply(L, layerDef.layer.args);
		}

		if(!layerDef.hasOwnProperty('id'))
			layerDef.id = L.stamp(layerDef.layer);

		if(layerDef.active)
			this._layersActives.push(layerDef.layer);

		this._layers.push(L.Util.extend(layerDef, {
			collapsed: isCollapsed,
			overlay: overlay,
			group: group
		}));

		if (this.options.autoZIndex && layerDef.layer && layerDef.layer.setZIndex) {
			this._lastZIndex++;
			layerDef.layer.setZIndex(this._lastZIndex);
		}

	},

	_createItem: function (obj) {

		var self = this;

		var item, input, checked;

		item = L.DomUtil.create('div', this.className + '-item' + (obj.active ? ' active' : ''));

		checked = this._map.hasLayer(obj.layer);

		if (obj.overlay) {
			if (typeof obj.exclusiveGroup !== "undefined"){
				input = this._createRadioElement(obj.exclusiveGroup.replace('', '_'), checked, obj);
			}else{
				input = L.DomUtil.create('input', this.className + '-selector');
				input.type = 'checkbox';
				input.defaultChecked = checked;
			}//TODO name
		} else
			input = this._createRadioElement('leaflet-base-layers', checked, obj);

		input.value = obj.id;
		input.layerId = obj.id;
		input.id = obj.id;
		input._layer = obj;

		L.DomEvent.on(input, 'click', function (e) {

			self._onInputClick();

			if (e.target.checked) {
				self.fire('panel:selected', e.target._layer);
			} else {
				self.fire('panel:unselected', e.target._layer);
			}

		}, this);

		var label = L.DomUtil.create('label', this.className + '-title');
		//TODO label.htmlFor = input.id;
		var title = L.DomUtil.create('span');
		title.innerHTML = obj.name || '';

		if (obj.icon) {
			var icon = L.DomUtil.create('i', this.className + '-icon');

			if (typeof obj.icon === 'string')
				icon.innerHTML = obj.icon || '';
			else
				icon.appendChild(obj.icon);

			label.appendChild(icon);
		}

		label.appendChild(input);
		label.appendChild(title);
		item.appendChild(label);

		if (this.options.buildItem) {
			var node = this.options.buildItem.call(this, obj); //custom node node or html string
			if (typeof node === 'string') {
				var tmp = L.DomUtil.create('div');
				tmp.innerHTML = node;
				item.appendChild(tmp.firstChild);
			}
			else
				item.appendChild(node);
		}

		this._items[input.value] = item;

		return item;
	},

	// IE7 bugs out if you create a radio dynamically, so you have to do it this hacky way (see http://bit.ly/PqYLBe)
	_createRadioElement: function (name, checked, obj) {

		var radioHtml = '<input type="radio" class="' + this.className + '-selector" name="' + name + '" id="' + obj.id + '"';
		if (checked) {
			radioHtml += ' checked="checked"';
		}
		radioHtml += ' />';

		var radioFragment = document.createElement('div');
		radioFragment.innerHTML = radioHtml;

		return radioFragment.firstChild;
	},

	_addItem: function (obj) {
		var self = this,
			label, input, icon, checked;

		var list = obj.overlay ? this._overlaysList : this._baseLayersList;

		if (obj.group) {
			if (!obj.group.hasOwnProperty('name'))
				obj.group = {name: obj.group};

			if (!this._groups[obj.group.name]) {
				var collapsed = false;
				if (obj.collapsed === true)
					collapsed = true;
				this._groups[obj.group.name] = this._createGroup(obj.group, collapsed, obj.overlay);
			}

			list.appendChild(this._groups[obj.group.name]);
			list = this._groups[obj.group.name];
		}

		label = this._createItem(obj);

		list.appendChild(label);

		return label;
	},

	_createGroup: function (groupdata, isCollapsed, isOverlay) {

		var self = this,
			groupdiv = L.DomUtil.create('div', this.className + '-group'),
			grouplabel, grouptit, groupexp, groupchb;

		grouplabel = L.DomUtil.create('label', this.className + '-grouplabel', groupdiv);

		// collapsible group
		if (this.options.collapsibleGroups) {
			// classes
			L.DomUtil.addClass(groupdiv, 'collapsible');

			// +/- icon
			groupexp = L.DomUtil.create('i', this.className + '-icon', grouplabel);
			if (isCollapsed === true)
				groupexp.innerHTML = ' + ';
			else
				groupexp.innerHTML = ' - ';

			// click on group
			L.DomEvent.on(grouplabel, 'click', function (e) {
				// do not trigger the checkbox that we might have
				e.stopPropagation();
				e.preventDefault();
				// collapse
				if (L.DomUtil.hasClass(groupdiv, 'expanded')) {
					L.DomUtil.removeClass(groupdiv, 'expanded');
					groupexp.innerHTML = ' + ';
				}
				// expand
				else {
					L.DomUtil.addClass(groupdiv, 'expanded');
					groupexp.innerHTML = ' - ';
				}
				// update the component's total height
				self._updateHeight();
			});

			if (isCollapsed === false)
				L.DomUtil.addClass(groupdiv, 'expanded');
		}

		// group name
		grouptit = L.DomUtil.create('span', this.className + '-title', grouplabel);
		grouptit.innerHTML = groupdata.name;

		// group with checkbox
		if (isOverlay && this.options.selectorGroup) {
			// create checkbox
			groupchb = L.DomUtil.create('input', this.className + '-selector', grouplabel);
			groupchb.type  = 'checkbox';
			groupchb.value = 'group';
			groupchb.name  = groupdata.name;
			groupchb.title = 'select all';
			groupchb.defaultChecked = false;
			// click on checkbox
			L.DomEvent.on(groupchb, 'click', this._onGroupClick, this);
		}

		return groupdiv;
	},

	_onGroupClick: function (e) {
		var i, j, input, obj,
			group     = e.target,
			inputs    = this._form.getElementsByClassName(this.className + '-selector'),
			inputsLen = inputs.length,
			changes   = [];

		// do not trigger the label that could collapse/expand the group
		e.stopPropagation();

		// all layer checkboxes
		for (i = 0; i < inputsLen; i++) {
			input = inputs[i];
			if (input.value == 'group') { continue; }
			obj = this._getLayer(input.value);
			if (obj.group && obj.group.name === group.name) {
				// change checkbox values
				if (input.checked !== group.checked) {
					changes.push(input);
					input.checked = group.checked;
				}
			}
		}

		// do the updates to the layers
		this._onInputClick();

		// fire the event that the layers were (un)selected to leaflet
		for (j in changes) {
			this.fire((group.checked ? 'panel:selected' : 'panel:unselected'), changes[j]._layer);
		}
	},

	_onInputClick: function () {
		var i, input, obj, key, g,
			inputs = this._form.getElementsByClassName(this.className + '-selector'),
			inputsLen = inputs.length,
			groups = {};

		// initialize groups
		for (key in this._groups) {
			groups[key] = {
				input   : null,
				checked : 0,
				total   : 0,
			};
		}

		this._handlingClick = true;

		// all layer checkboxes
		for (i = 0; i < inputsLen; i++) {

			input = inputs[i];

			if (input.value == 'group') {
				// remember group input
				groups[input.name].input = input;
				continue;
			}

			obj = this._getLayer(input.value);

			// if the obj is part of a group, count up
			if (obj.group) {
				groups[obj.group.name].total++;
				if (input.checked) {
					groups[obj.group.name].checked++;
				}
			}

			// add the layer to the map
			if (input.checked && !this._map.hasLayer(obj.layer)) {
				L.DomUtil.addClass(input.parentNode.parentNode, 'active');
				this._map.addLayer(obj.layer);
			}
			// remove the layer from the map
			else if (!input.checked && this._map.hasLayer(obj.layer)) {
				L.DomUtil.removeClass(input.parentNode.parentNode, 'active');
				this._map.removeLayer(obj.layer);
			}
		}

		this._handlingClick = false;

		// update all groups that have checkboxes
		for (key in groups) {
			g = groups[key];
			if (! g.input) { continue; }

			// all
			if (g.checked === g.total) {
				g.input.indeterminate = false;
				g.input.checked = true;
			}
			// none
			else if (g.checked === 0) {
				g.input.indeterminate = false;
				g.input.checked = false;
			}
			// some
			else {
				g.input.indeterminate = true;
				// determine whether the next click selects or deselects all by the amount
				g.input.checked = (g.checked / g.total) >= 0.5;
			}
		}

		this._refocusOnMap();
	},


	_initLayout: function () {
		var container = this._container = L.DomUtil.create('div', this.className);

		if(this.options.compact)
			L.DomUtil.addClass(container, 'compact');

		//Makes this work on IE10 Touch devices by stopping it from firing a mouseout event when the touch is released
		container.setAttribute('aria-haspopup', true);

		L.DomEvent
			.disableClickPropagation(container)
			.disableScrollPropagation(container);

		if (this.options.className)
			L.DomUtil.addClass(container, this.options.className);

		this._section = this._form = L.DomUtil.create('form', this.className + '-list');

		this._updateHeight();

		if (this.options.collapsed) {

			if (L.Browser.android)
				L.DomEvent
					.on(container, 'click', this._expand, this);
			else {
				L.DomEvent
					.on(container, 'mouseenter', this._expand, this)
					.on(container, 'mouseleave', this._collapse, this);
			}

			this._map.on('click', this._collapse, this);

		} else {
			this._expand();
		}

		this._baseLayersList = L.DomUtil.create('div', this.className + '-base', this._form);
		this._separator = L.DomUtil.create('div', this.className + '-separator', this._form);
		this._overlaysList = L.DomUtil.create('div', this.className + '-overlays', this._form);

		/* maybe useless
		if (!this.options.compact)
			L.DomUtil.create('div', this.className + '-margin', this._form);*/

		if (this.options.title) {
			var titlabel = L.DomUtil.create('label', this.className + '-title');
			titlabel.innerHTML = '<span>' + this.options.title + '</span>';
			container.appendChild(titlabel);
		}

		container.appendChild(this._form);
	},

	_updateHeight: function (h) {
		h = h || this._map.getSize().y;

		if (this.options.compact)
			this._form.style.maxHeight = (h - this.options.compactOffset) + 'px';
		else
			this._form.style.height = h + 'px';
	},

	_expand: function () {
		L.DomUtil.addClass(this._container, 'expanded');
	},

	_collapse: function () {
		this._container.className = this._container.className.replace('expanded', '');
	},

	_getPath: function (obj, prop) {
		var parts = prop.split('.'),
			last = parts.pop(),
			len = parts.length,
			cur = parts[0],
			i = 1;

		if (len > 0)
			while ((obj = obj[cur]) && i < len)
				cur = parts[i++];

		if (obj)
			return obj[last];
	}
});

L.control.panelLayers = function (baseLayers, overlays, options) {
	return new L.Control.PanelLayers(baseLayers, overlays, options);
};

return L.Control.PanelLayers;

});