/* global _wpWidgetCustomizerPreviewSettings */
wp.customize.widgetsPreview = wp.customize.WidgetCustomizerPreview = (function( $, _, wp, api ) {

	var self;

	self = {
		renderedSidebars: {},
		renderedWidgets: {},
		registeredSidebars: [],
		registeredWidgets: {},
		widgetSelectors: [],
		preview: null,
		l10n: {
			widgetTooltip: ''
		}
	};

	/**
	 * Init widgets preview.
	 *
	 * @since 4.5.0
	 */
	self.init = function() {
		var self = this;

		self.preview = api.preview;
		if ( api.selectiveRefresh ) {
			self.addPartials();
		}

		self.buildWidgetSelectors();
		self.highlightControls();

		self.preview.bind( 'highlight-widget', self.highlightWidget );

		api.preview.bind( 'active', function() {
			self.highlightControls();
		} );
	};

	if ( api.selectiveRefresh ) {

		/**
		 * Partial representing a widget instance.
		 *
		 * @class
		 * @augments wp.customize.selectiveRefresh.Partial
		 * @since 4.5.0
		 */
		self.WidgetPartial = api.selectiveRefresh.Partial.extend({

			/**
			 * Constructor.
			 *
			 * @since 4.5.0
			 * @param {string} id - Partial ID.
			 * @param {Object} options
			 * @param {Object} options.params
			 */
			initialize: function( id, options ) {
				var partial = this, matches;
				matches = id.match( /^widget\[(.+)]$/ );
				if ( ! matches ) {
					throw new Error( 'Illegal id for widget partial.' );
				}

				partial.widgetId = matches[1];
				options = options || {};
				options.params = _.extend(
					{
						/* Note that a selector of ('#' + partial.widgetId) is faster, but jQuery will only return the one result. */
						selector: '[id="' + partial.widgetId + '"]', // Alternatively, '[data-customize-widget-id="' + partial.widgetId + '"]'
						settings: [ self.getWidgetSettingId( partial.widgetId ) ],
						containerInclusive: true
					},
					options.params || {}
				);

				api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );
			},

			/**
			 * Send widget-updated message to parent so spinner will get removed from widget control.
			 *
			 * @inheritdoc
			 * @param {wp.customize.selectiveRefresh.Placement} placement
			 */
			renderContent: function( placement ) {
				var partial = this;
				if ( api.selectiveRefresh.Partial.prototype.renderContent.call( partial, placement ) ) {
					api.preview.send( 'widget-updated', partial.widgetId );
					api.selectiveRefresh.trigger( 'widget-updated', partial );
				}
			}
		});

		/**
		 * Partial representing a widget area.
		 *
		 * @class
		 * @augments wp.customize.selectiveRefresh.Partial
		 * @since 4.5.0
		 */
		self.SidebarPartial = api.selectiveRefresh.Partial.extend({

			/**
			 * Constructor.
			 *
			 * @since 4.5.0
			 * @param {string} id - Partial ID.
			 * @param {Object} options
			 * @param {Object} options.params
			 */
			initialize: function( id, options ) {
				var partial = this, matches;
				matches = id.match( /^sidebar\[(.+)]$/ );
				if ( ! matches ) {
					throw new Error( 'Illegal id for sidebar partial.' );
				}
				partial.sidebarId = matches[1];

				options = options || {};
				options.params = _.extend(
					{
						settings: [ 'sidebars_widgets[' + partial.sidebarId + ']' ]
					},
					options.params || {}
				);

				api.selectiveRefresh.Partial.prototype.initialize.call( partial, id, options );

				if ( ! partial.params.sidebarArgs ) {
					throw new Error( 'The sidebarArgs param was not provided.' );
				}
				if ( partial.params.settings.length > 1 ) {
					throw new Error( 'Expected SidebarPartial to only have one associated setting' );
				}
			},

			/**
			 * Set up the partial.
			 *
			 * @since 4.5.0
			 */
			ready: function() {
				var sidebarPartial = this;

				// Watch for changes to the sidebar_widgets setting.
				_.each( sidebarPartial.settings(), function( settingId ) {
					api( settingId ).bind( _.bind( sidebarPartial.handleSettingChange, sidebarPartial ) );
				} );

				// Trigger an event for this sidebar being updated whenever a widget inside is rendered.
				api.selectiveRefresh.bind( 'partial-content-rendered', function( placement ) {
					var isAssignedWidgetPartial = (
						placement.partial.extended( self.WidgetPartial ) &&
						( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), placement.partial.widgetId ) )
					);
					if ( isAssignedWidgetPartial ) {
						api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
					}
				} );

				// Make sure that a widget partial has a container in the DOM prior to a refresh.
				api.bind( 'change', function( widgetSetting ) {
					var widgetId, parsedId;
					parsedId = self.parseWidgetSettingId( widgetSetting.id );
					if ( ! parsedId ) {
						return;
					}
					widgetId = parsedId.idBase;
					if ( parsedId.number ) {
						widgetId += '-' + String( parsedId.number );
					}
					if ( -1 !== _.indexOf( sidebarPartial.getWidgetIds(), widgetId ) ) {
						sidebarPartial.ensureWidgetPlacementContainers( widgetId );
					}
				} );
			},

			/**
			 * Get the before/after boundary nodes for all instances of this sidebar (usually one).
			 *
			 * Note that TreeWalker is not implemented in IE8.
			 *
			 * @since 4.5.0
			 * @returns {Array.<{before: Comment, after: Comment, instanceNumber: number}>}
			 */
			findDynamicSidebarBoundaryNodes: function() {
				var partial = this, regExp, boundaryNodes = {}, recursiveCommentTraversal;
				regExp = /^(dynamic_sidebar_before|dynamic_sidebar_after):(.+):(\d+)$/;
				recursiveCommentTraversal = function( childNodes ) {
					_.each( childNodes, function( node ) {
						var matches;
						if ( 8 === node.nodeType ) {
							matches = node.nodeValue.match( regExp );
							if ( ! matches || matches[2] !== partial.sidebarId ) {
								return;
							}
							if ( _.isUndefined( boundaryNodes[ matches[3] ] ) ) {
								boundaryNodes[ matches[3] ] = {
									before: null,
									after: null,
									instanceNumber: parseInt( matches[3], 10 )
								};
							}
							if ( 'dynamic_sidebar_before' === matches[1] ) {
								boundaryNodes[ matches[3] ].before = node;
							} else {
								boundaryNodes[ matches[3] ].after = node;
							}
						} else if ( 1 === node.nodeType ) {
							recursiveCommentTraversal( node.childNodes );
						}
					} );
				};

				recursiveCommentTraversal( document.body.childNodes );
				return _.values( boundaryNodes );
			},

			/**
			 * Get the placements for this partial.
			 *
			 * @since 4.5.0
			 * @returns {Array}
			 */
			placements: function() {
				var partial = this;
				return _.map( partial.findDynamicSidebarBoundaryNodes(), function( boundaryNodes ) {
					return new api.selectiveRefresh.Placement( {
						partial: partial,
						container: null,
						startNode: boundaryNodes.before,
						endNode: boundaryNodes.after,
						context: {
							instanceNumber: boundaryNodes.instanceNumber
						}
					} );
				} );
			},

			/**
			 * Get the list of widget IDs associated with this widget area.
			 *
			 * @since 4.5.0
			 *
			 * @returns {Array}
			 */
			getWidgetIds: function() {
				var sidebarPartial = this, settingId, widgetIds;
				settingId = sidebarPartial.settings()[0];
				if ( ! settingId ) {
					throw new Error( 'Missing associated setting.' );
				}
				if ( ! api.has( settingId ) ) {
					throw new Error( 'Setting does not exist.' );
				}
				widgetIds = api( settingId ).get();
				if ( ! _.isArray( widgetIds ) ) {
					throw new Error( 'Expected setting to be array of widget IDs' );
				}
				return widgetIds.slice( 0 );
			},

			/**
			 * Reflow widgets in the sidebar, ensuring they have the proper position in the DOM.
			 *
			 * @since 4.5.0
			 *
			 * @return {Array.<wp.customize.selectiveRefresh.Placement>} List of placements that were reflowed.
			 */
			reflowWidgets: function() {
				var sidebarPartial = this, sidebarPlacements, widgetIds, widgetPartials, sortedSidebarContainers = [];
				widgetIds = sidebarPartial.getWidgetIds();
				sidebarPlacements = sidebarPartial.placements();

				widgetPartials = {};
				_.each( widgetIds, function( widgetId ) {
					var widgetPartial = api.selectiveRefresh.partial( 'widget[' + widgetId + ']' );
					if ( widgetPartial ) {
						widgetPartials[ widgetId ] = widgetPartial;
					}
				} );

				_.each( sidebarPlacements, function( sidebarPlacement ) {
					var sidebarWidgets = [], needsSort = false, thisPosition, lastPosition = -1;

					// Gather list of widget partial containers in this sidebar, and determine if a sort is needed.
					_.each( widgetPartials, function( widgetPartial ) {
						_.each( widgetPartial.placements(), function( widgetPlacement ) {

							if ( sidebarPlacement.context.instanceNumber === widgetPlacement.context.sidebar_instance_number ) {
								thisPosition = widgetPlacement.container.index();
								sidebarWidgets.push( {
									partial: widgetPartial,
									placement: widgetPlacement,
									position: thisPosition
								} );
								if ( thisPosition < lastPosition ) {
									needsSort = true;
								}
								lastPosition = thisPosition;
							}
						} );
					} );

					if ( needsSort ) {
						_.each( sidebarWidgets, function( sidebarWidget ) {
							sidebarPlacement.endNode.parentNode.insertBefore(
								sidebarWidget.placement.container[0],
								sidebarPlacement.endNode
							);

							// @todo Rename partial-placement-moved?
							api.selectiveRefresh.trigger( 'partial-content-moved', sidebarWidget.placement );
						} );

						sortedSidebarContainers.push( sidebarPlacement );
					}
				} );

				if ( sortedSidebarContainers.length > 0 ) {
					api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
				}

				return sortedSidebarContainers;
			},

			/**
			 * Make sure there is a widget instance container in this sidebar for the given widget ID.
			 *
			 * @since 4.5.0
			 *
			 * @param {string} widgetId
			 * @returns {wp.customize.selectiveRefresh.Partial} Widget instance partial.
			 */
			ensureWidgetPlacementContainers: function( widgetId ) {
				var sidebarPartial = this, widgetPartial, wasInserted = false, partialId = 'widget[' + widgetId + ']';
				widgetPartial = api.selectiveRefresh.partial( partialId );
				if ( ! widgetPartial ) {
					widgetPartial = new self.WidgetPartial( partialId, {
						params: {}
					} );
					api.selectiveRefresh.partial.add( widgetPartial.id, widgetPartial );
				}

				// Make sure that there is a container element for the widget in the sidebar, if at least a placeholder.
				_.each( sidebarPartial.placements(), function( sidebarPlacement ) {
					var foundWidgetPlacement, widgetContainerElement;

					foundWidgetPlacement = _.find( widgetPartial.placements(), function( widgetPlacement ) {
						return ( widgetPlacement.context.sidebar_instance_number === sidebarPlacement.context.instanceNumber );
					} );
					if ( foundWidgetPlacement ) {
						return;
					}

					widgetContainerElement = $(
						sidebarPartial.params.sidebarArgs.before_widget.replace( '%1$s', widgetId ).replace( '%2$s', 'widget' ) +
						sidebarPartial.params.sidebarArgs.after_widget
					);

					widgetContainerElement.attr( 'data-customize-partial-id', widgetPartial.id );
					widgetContainerElement.attr( 'data-customize-partial-type', 'widget' );
					widgetContainerElement.attr( 'data-customize-widget-id', widgetId );

					/*
					 * Make sure the widget container element has the customize-container context data.
					 * The sidebar_instance_number is used to disambiguate multiple instances of the
					 * same sidebar are rendered onto the template, and so the same widget is embedded
					 * multiple times.
					 */
					widgetContainerElement.data( 'customize-partial-placement-context', {
						'sidebar_id': sidebarPartial.sidebarId,
						'sidebar_instance_number': sidebarPlacement.context.instanceNumber
					} );

					sidebarPlacement.endNode.parentNode.insertBefore( widgetContainerElement[0], sidebarPlacement.endNode );
					wasInserted = true;
				} );

				if ( wasInserted ) {
					sidebarPartial.reflowWidgets();
				}

				return widgetPartial;
			},

			/**
			 * Handle change to the sidebars_widgets[] setting.
			 *
			 * @since 4.5.0
			 *
			 * @param {Array} newWidgetIds New widget ids.
			 * @param {Array} oldWidgetIds Old widget ids.
			 */
			handleSettingChange: function( newWidgetIds, oldWidgetIds ) {
				var sidebarPartial = this, needsRefresh, widgetsRemoved, widgetsAdded, addedWidgetPartials = [];

				needsRefresh = (
					( oldWidgetIds.length > 0 && 0 === newWidgetIds.length ) ||
					( newWidgetIds.length > 0 && 0 === oldWidgetIds.length )
				);
				if ( needsRefresh ) {
					sidebarPartial.fallback();
					return;
				}

				// Handle removal of widgets.
				widgetsRemoved = _.difference( oldWidgetIds, newWidgetIds );
				_.each( widgetsRemoved, function( removedWidgetId ) {
					var widgetPartial = api.selectiveRefresh.partial( 'widget[' + removedWidgetId + ']' );
					if ( widgetPartial ) {
						_.each( widgetPartial.placements(), function( placement ) {
							var isRemoved = (
								placement.context.sidebar_id === sidebarPartial.sidebarId ||
								( placement.context.sidebar_args && placement.context.sidebar_args.id === sidebarPartial.sidebarId )
							);
							if ( isRemoved ) {
								placement.container.remove();
							}
						} );
					}
				} );

				// Handle insertion of widgets.
				widgetsAdded = _.difference( newWidgetIds, oldWidgetIds );
				_.each( widgetsAdded, function( addedWidgetId ) {
					var widgetPartial = sidebarPartial.ensureWidgetPlacementContainers( addedWidgetId );
					addedWidgetPartials.push( widgetPartial );
				} );

				_.each( addedWidgetPartials, function( widgetPartial ) {
					widgetPartial.refresh();
				} );

				api.selectiveRefresh.trigger( 'sidebar-updated', sidebarPartial );
			},

			/**
			 * Note that the meat is handled in handleSettingChange because it has the context of which widgets were removed.
			 *
			 * @since 4.5.0
			 */
			refresh: function() {
				var partial = this, deferred = $.Deferred();

				deferred.fail( function() {
					partial.fallback();
				} );

				if ( 0 === partial.placements().length ) {
					deferred.reject();
				} else {
					_.each( partial.reflowWidgets(), function( sidebarPlacement ) {
						api.selectiveRefresh.trigger( 'partial-content-rendered', sidebarPlacement );
					} );
					deferred.resolve();
				}

				return deferred.promise();
			}
		});

		api.selectiveRefresh.partialConstructor.sidebar = self.SidebarPartial;
		api.selectiveRefresh.partialConstructor.widget = self.WidgetPartial;

		/**
		 * Add partials for the registered widget areas (sidebars).
		 *
		 * @since 4.5.0
		 */
		self.addPartials = function() {
			_.each( self.registeredSidebars, function( registeredSidebar ) {
				var partial, partialId = 'sidebar[' + registeredSidebar.id + ']';
				partial = api.selectiveRefresh.partial( partialId );
				if ( ! partial ) {
					partial = new self.SidebarPartial( partialId, {
						params: {
							sidebarArgs: registeredSidebar
						}
					} );
					api.selectiveRefresh.partial.add( partial.id, partial );
				}
			} );
		};

	}

	/**
	 * Calculate the selector for the sidebar's widgets based on the registered sidebar's info.
	 *
	 * @since 3.9.0
	 */
	self.buildWidgetSelectors = function() {
		var self = this;

		$.each( self.registeredSidebars, function( i, sidebar ) {
			var widgetTpl = [
					sidebar.before_widget.replace( '%1$s', '' ).replace( '%2$s', '' ),
					sidebar.before_title,
					sidebar.after_title,
					sidebar.after_widget
				].join( '' ),
				emptyWidget,
				widgetSelector,
				widgetClasses;

			emptyWidget = $( widgetTpl );
			widgetSelector = emptyWidget.prop( 'tagName' );
			widgetClasses = emptyWidget.prop( 'className' );

			// Prevent a rare case when before_widget, before_title, after_title and after_widget is empty.
			if ( ! widgetClasses ) {
				return;
			}

			widgetClasses = widgetClasses.replace( /^\s+|\s+$/g, '' );

			if ( widgetClasses ) {
				widgetSelector += '.' + widgetClasses.split( /\s+/ ).join( '.' );
			}
			self.widgetSelectors.push( widgetSelector );
		});
	};

	/**
	 * Highlight the widget on widget updates or widget control mouse overs.
	 *
	 * @since 3.9.0
	 * @param  {string} widgetId ID of the widget.
	 */
	self.highlightWidget = function( widgetId ) {
		var $body = $( document.body ),
			$widget = $( '#' + widgetId );

		$body.find( '.widget-customizer-highlighted-widget' ).removeClass( 'widget-customizer-highlighted-widget' );

		$widget.addClass( 'widget-customizer-highlighted-widget' );
		setTimeout( function() {
			$widget.removeClass( 'widget-customizer-highlighted-widget' );
		}, 500 );
	};

	/**
	 * Show a title and highlight widgets on hover. On shift+clicking
	 * focus the widget control.
	 *
	 * @since 3.9.0
	 */
	self.highlightControls = function() {
		var self = this,
			selector = this.widgetSelectors.join( ',' );

		$( selector ).attr( 'title', this.l10n.widgetTooltip );

		$( document ).on( 'mouseenter', selector, function() {
			self.preview.send( 'highlight-widget-control', $( this ).prop( 'id' ) );
		});

		// Open expand the widget control when shift+clicking the widget element
		$( document ).on( 'click', selector, function( e ) {
			if ( ! e.shiftKey ) {
				return;
			}
			e.preventDefault();

			self.preview.send( 'focus-widget-control', $( this ).prop( 'id' ) );
		});
	};

	/**
	 * Parse a widget ID.
	 *
	 * @since 4.5.0
	 *
	 * @param {string} widgetId Widget ID.
	 * @returns {{idBase: string, number: number|null}}
	 */
	self.parseWidgetId = function( widgetId ) {
		var matches, parsed = {
			idBase: '',
			number: null
		};

		matches = widgetId.match( /^(.+)-(\d+)$/ );
		if ( matches ) {
			parsed.idBase = matches[1];
			parsed.number = parseInt( matches[2], 10 );
		} else {
			parsed.idBase = widgetId; // Likely an old single widget.
		}

		return parsed;
	};

	/**
	 * Parse a widget setting ID.
	 *
	 * @since 4.5.0
	 *
	 * @param {string} settingId Widget setting ID.
	 * @returns {{idBase: string, number: number|null}|null}
	 */
	self.parseWidgetSettingId = function( settingId ) {
		var matches, parsed = {
			idBase: '',
			number: null
		};

		matches = settingId.match( /^widget_([^\[]+?)(?:\[(\d+)])?$/ );
		if ( ! matches ) {
			return null;
		}
		parsed.idBase = matches[1];
		if ( matches[2] ) {
			parsed.number = parseInt( matches[2], 10 );
		}
		return parsed;
	};

	/**
	 * Convert a widget ID into a Customizer setting ID.
	 *
	 * @since 4.5.0
	 *
	 * @param {string} widgetId Widget ID.
	 * @returns {string} settingId Setting ID.
	 */
	self.getWidgetSettingId = function( widgetId ) {
		var parsed = this.parseWidgetId( widgetId ), settingId;

		settingId = 'widget_' + parsed.idBase;
		if ( parsed.number ) {
			settingId += '[' + String( parsed.number ) + ']';
		}

		return settingId;
	};

	api.bind( 'preview-ready', function() {
		$.extend( self, _wpWidgetCustomizerPreviewSettings );
		self.init();
	});

	return self;
})( jQuery, _, wp, wp.customize );
