(function ($) {
	$( window ).on( 'elementor/frontend/init', () => {
		class ImageGalleryWidget extends elementorModules.frontend.handlers.Base {
			getDefaultSettings() {
				return {
					selectors: {
						container: '.pp-image-gallery-container',
						gallery: '.pp-image-gallery',
						justifiedGallery: '.pp-image-gallery-justified',
						filterItems: '.pp-gallery-filter',
					},
				};
			}

			getDefaultElements() {
				const selectors = this.getSettings( 'selectors' );
				return {
					$container: this.$element.find( selectors.container ),
					$gallery: this.$element.find( selectors.gallery ),
					$justifiedGallery: this.$element.find( selectors.justifiedGallery ),
					$filterItems: this.$element.find( selectors.filterItems ),
				};
			}

			bindEvents() {
				const settings  = this.elements.$container.data('settings'),
					gallery     = this.elements.$gallery,
					cachedItems = [],
					cachedIds   = [];

				if ( ! settings ) {
					return;
				}

				const isMasonry = gallery.hasClass('pp-image-gallery-masonry');

				if ( isMasonry ) {
					this.masonry = new PPMasonry( {
						container: gallery[0],
						itemSelector: '.pp-grid-item-wrap',
						// Items mid filter-animation are pinned absolutely; skip them.
						excludeSelector: '.pp-grid-item-exiting',
					} );

					this.masonry.observeResize();

					this.initMasonryLayout();
				} else if ( settings.layout !== 'justified' ) {
					// Grid uses native CSS grid — no JS layout needed, just apply the default filter.
					const defaultFilter = this.getDefaultFilter();
					if ( defaultFilter ) {
						this.applyFilter( defaultFilter );
					}
				}

				if ( gallery.hasClass('pp-image-gallery-filter-enabled') ) {
					this.initFilters(gallery, settings);
				}

				if ( ! elementorFrontend.isEditMode() && settings.pagination === 'yes' ) {
					this.initLoadMore(gallery, settings, cachedItems, cachedIds);
				}

				if ( settings.tilt_enable === 'yes' ) {
					this.initTilt();
				}

				if ( settings.layout === 'justified' ) {
					this.initjustifiedLayout();
				}

				this.initLightbox();
			}

			/**
			 * Read the default filter value from the filter buttons, if any.
			 */
			getDefaultFilter() {
				let defaultFilter = '';
				$( this.elements.$filterItems ).each( function () {
					if ( defaultFilter === '' || defaultFilter === undefined ) {
						defaultFilter = $( this ).attr('data-default');
					}
				} );
				return defaultFilter;
			}

			/**
			 * Masonry layout: absolutely-positioned items packed into the shortest column,
			 * matching Elementor's Gallery widget. Only runs for the masonry layout; grid uses
			 * native CSS grid with no JS.
			 */
			initMasonryLayout() {
				const self = this;

				// Aspect-ratios are reserved in the markup, so we can lay out immediately.
				this.masonry.setReady();

				// Apply the initial default filter, if any.
				const defaultFilter = this.getDefaultFilter();

				// Lay out on the next frame so the container has its final width (important in the
				// editor on first load); the reserved aspect-ratios keep it shift-free.
				requestAnimationFrame( function () {
					if ( defaultFilter ) {
						self.applyFilter( defaultFilter );
					} else {
						self.masonry.layout();
					}
				} );

				// Re-run once everything is loaded, and as each (lazy) image resolves, as a
				// safety net for images without reserved dimensions (e.g. SVGs).
				this.elements.$container.imagesLoaded( function () {
					self.masonry.layout();
				} );

				self.elements.$gallery.find('.pp-gallery-slide-image').on('load', function () {
					self.masonry.layout();
				} );

				elementorFrontend.elements.$window.on('elementor-pro/motion-fx/recalc', function () {
					self.masonry.layout();
				} );

				const triggers = [
					'ppe-tabs-switched',
					'ppe-toggle-switched',
					'ppe-accordion-switched',
					'ppe-popup-opened',
				];

				triggers.forEach( function ( trigger ) {
					$( document ).on( trigger, function ( e, wrap ) {
						if ( 'ppe-popup-opened' === trigger ) {
							wrap = $( '.pp-modal-popup-' + wrap );
						}

						if ( wrap && wrap.find( '.pp-image-gallery' ).length > 0 ) {
							setTimeout( function () {
								self.masonry.layout();
							}, 100 );
						}
					} );
				} );
			}

			/**
			 * Filter animation duration in ms, read from the `--pp-gallery-filter-duration` CSS
			 * var (default 400ms). Returns 0 when the user prefers reduced motion.
			 */
			getFilterDuration() {
				if ( window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches ) {
					return 0;
				}

				const raw = getComputedStyle( this.elements.$gallery[0] ).getPropertyValue('--pp-gallery-filter-duration').trim();

				if ( raw.endsWith('ms') ) {
					return parseFloat( raw ) || 0;
				}
				if ( raw.endsWith('s') ) {
					return ( parseFloat( raw ) || 0 ) * 1000;
				}
				return 400;
			}

			/**
			 * Show/hide items for a filter value, then relayout.
			 *
			 * @param {string}  filterValue A jQuery selector (e.g. '.category-slug') or '*' for all.
			 * @param {boolean} animate     Animate the transition (FLIP + scale/fade). Off for the
			 *                              initial/default filter so there's no load-in animation.
			 */
			applyFilter( filterValue, animate ) {
				const gallery   = this.elements.$gallery,
					galleryEl   = gallery[0],
					allItems    = Array.prototype.slice.call( gallery.children('.pp-grid-item-wrap') ),
					isMasonry   = gallery.hasClass('pp-image-gallery-masonry'),
					matches     = ( el ) => ! filterValue || '*' === filterValue || el.matches( filterValue ),
					duration    = animate ? this.getFilterDuration() : 0;

				// Finalise any in-progress animation so we start from a clean, settled state.
				this.finalizeFilter();

				if ( ! duration ) {
					allItems.forEach( ( el ) => el.classList.toggle( 'pp-grid-item-hidden', ! matches( el ) ) );
					if ( this.masonry ) {
						this.masonry.layout();
					}
					return;
				}

				const galleryRect = galleryEl.getBoundingClientRect(),
					nowVisible    = allItems.filter( ( el ) => ! el.classList.contains('pp-grid-item-hidden') ),
					first         = new Map();

				// FIRST: remember where the currently-visible items are.
				nowVisible.forEach( ( el ) => first.set( el, el.getBoundingClientRect() ) );

				const staying  = nowVisible.filter( matches ),
					exiting    = nowVisible.filter( ( el ) => ! matches( el ) ),
					entering   = allItems.filter( ( el ) => el.classList.contains('pp-grid-item-hidden') && matches( el ) );

				// Pin exiting items out of flow at their current spot so the layout recomputes
				// without them, then fade + scale them out.
				exiting.forEach( ( el ) => {
					const r = first.get( el );
					el.classList.add('pp-grid-item-exiting');
					el.style.transition = 'none';
					el.style.position = 'absolute';
					el.style.margin = '0';
					el.style.zIndex = '0';
					el.style.width = r.width + 'px';
					el.style.top = ( r.top - galleryRect.top ) + 'px';
					el.style.left = ( r.left - galleryRect.left ) + 'px';
					el.style.right = 'auto';
				} );

				// Bring entering items into the layout (still invisible; animated in below).
				entering.forEach( ( el ) => el.classList.remove('pp-grid-item-hidden') );

				// Recompute positions. Masonry repacks; grid reflows on its own via CSS.
				if ( isMasonry && this.masonry ) {
					this.masonry.layout();
				}

				// PLAY: exit.
				exiting.forEach( ( el ) => {
					el.style.transition = `transform ${duration}ms ease, opacity ${duration}ms ease`;
					el.style.transformOrigin = 'center center';
					el.style.transform = 'scale(0.5)';
					el.style.opacity = '0';
					el.style.pointerEvents = 'none';
				} );

				// LAST + INVERT: place staying items back at their old spot with a transform.
				staying.forEach( ( el ) => {
					const f = first.get( el ),
						last = el.getBoundingClientRect(),
						dx = f.left - last.left,
						dy = f.top - last.top;

					el.style.transition = 'none';
					el.style.transform = `translate(${dx}px, ${dy}px)`;
				} );

				// Entering items start shrunk/invisible.
				entering.forEach( ( el ) => {
					el.style.transition = 'none';
					el.style.transformOrigin = 'center center';
					el.style.transform = 'scale(0.5)';
					el.style.opacity = '0';
				} );

				// Force a single reflow so the inverted/initial states are committed…
				galleryEl.offsetWidth; // eslint-disable-line no-unused-expressions

				// …then PLAY the staying + entering items to their natural state.
				staying.forEach( ( el ) => {
					el.style.transition = `transform ${duration}ms ease`;
					el.style.transform = '';
				} );
				entering.forEach( ( el ) => {
					el.style.transition = `transform ${duration}ms ease, opacity ${duration}ms ease`;
					el.style.transform = '';
					el.style.opacity = '';
				} );

				this._filterAnim = { exiting, staying, entering, isMasonry };
				this._filterAnimTimer = setTimeout( () => this.finalizeFilter(), duration + 60 );
			}

			/**
			 * Clear any in-progress filter animation: strip inline animation styles and settle
			 * exiting items into their final hidden state. Safe to call when nothing is running.
			 */
			finalizeFilter() {
				if ( this._filterAnimTimer ) {
					clearTimeout( this._filterAnimTimer );
					this._filterAnimTimer = null;
				}

				const anim = this._filterAnim;
				if ( ! anim ) {
					return;
				}
				this._filterAnim = null;

				anim.exiting.forEach( ( el ) => {
					el.classList.remove('pp-grid-item-exiting');
					el.classList.add('pp-grid-item-hidden');
					el.style.cssText = '';
				} );
				anim.staying.forEach( ( el ) => {
					el.style.transition = '';
					el.style.transform = '';
				} );
				anim.entering.forEach( ( el ) => {
					el.style.transition = '';
					el.style.transform = '';
					el.style.opacity = '';
					el.style.transformOrigin = '';
				} );

				// Re-settle masonry now the inline transforms are gone.
				if ( anim.isMasonry && this.masonry ) {
					this.masonry.layout();
				}
			}

			initFilters(gallery, settings) {
				const widgetId      = this.getID(),
					lightboxLibrary = this.getElementSettings('lightbox_library'),
					self            = this;

				this.elements.$container.on( 'click', '.pp-gallery-filter', function() {
					let $this = $(this),
						filterValue = $this.attr('data-filter'),
						filterIndex = $this.attr('data-gallery-index'),
						galleryItems = gallery.find(filterValue);

					if ( filterValue === '*' ) {
						galleryItems = gallery.find('.pp-grid-item-wrap');
					}

					$(galleryItems).each(function() {
						let imgLink = $(this).find('.pp-image-gallery-item-link');

						if ( lightboxLibrary === 'fancybox' ) {
							imgLink.attr('data-fancybox', filterIndex + '_' + widgetId);
						} else {
							imgLink.attr('data-elementor-lightbox-slideshow', filterIndex + '_' + widgetId);
						}
					});

					$this.siblings().removeClass('pp-active');
					$this.addClass('pp-active');

					if ( settings.layout === 'justified' ) {
						self.initjustifiedLayout(filterValue);
					} else {
						self.applyFilter(filterValue, true);
					}
				});

				$('.pp-filters-dropdown').on( 'change', function() {
					// get filter value from option value.
					let filterValue = this.value,
						filterIndex = $(this).find(':selected').attr('data-gallery-index'),
						galleryItems = gallery.find(filterValue);

					if ( filterValue === '*' ) {
						galleryItems = gallery.find('.pp-grid-item-wrap');
					}

					$(galleryItems).each(function() {
						let imgLink = $(this).find('.pp-image-gallery-item-link');

						if ( lightboxLibrary === 'fancybox' ) {
							imgLink.attr('data-fancybox', filterIndex + '_' + widgetId);
						} else {
							imgLink.attr('data-elementor-lightbox-slideshow', filterIndex + '_' + widgetId);
						}
					});

					if ( settings.layout === 'justified' ) {
						self.initjustifiedLayout(filterValue);
					} else {
						self.applyFilter(filterValue, true);
					}
				});

				// Trigger filter by hash parameter in URL.
				this.hashChange();

				// Trigger filter on hash change in URL.
				$(window).on( 'hashchange', function() {
					this.hashChange();
				}.bind(this) );
			}

			initjustifiedLayout(filterValue) {
				const settings       = this.elements.$container.data('settings'),
					justifiedGallery = this.elements.$justifiedGallery;
				let opts = {
					rowHeight : settings.row_height,
					lastRow : settings.last_row,
					selector : 'div',
					waitThumbnailsLoad : true,
					margins : settings.image_spacing,
					border : 0
				};

				if ( filterValue ) {
					opts.filter = filterValue;
				}

				justifiedGallery.imagesLoaded( function () {
					justifiedGallery.justifiedGallery(opts).on('jg.complete jg.resize', function(e) {
						let controller = $(this).data('jg.controller');

						if ( controller.rows === 0 && controller.settings.lastRow === 'hide' ){
							opts.lastRow = 'justify';

							$(this).justifiedGallery(opts);
						}
					});
				}.bind( this ) );
			}

			initLoadMore(gallery, settings, cachedItems, cachedIds) {
				const self = this;

				gallery.find('.pp-grid-item-wrap').each(function() {
					cachedIds.push( $(this).data('item-id') );
				});

				this.elements.$container.find('.pp-gallery-load-more').off('click').on('click', function(e) {
					e.preventDefault();

					let $this = $(this);
					$this.addClass('disabled pp-loading');

					if ( cachedItems.length > 0 ) {
						self.renderGalleryItems(cachedItems, cachedIds);
					} else {
						self.getAjaxPhotos(cachedItems, cachedIds, settings);
					}
				});
			}

			getAjaxPhotos(cachedItems, cachedIds, settings) {
				const self = this;

				let data = {
					action: 'pp_gallery_get_images',
					pp_action: 'pp_gallery_get_images',
					settings: settings
				};

				$.ajax({
					type: 'post',
					url: window.location.href.split( '#' ).shift(),
					context: this,
					data: data,
					success: function(response) {
						if ( response.success ) {
							let items = response.data.items;

							if ( items ) {
								$(items).each(function() {
									if ( $(this).hasClass('pp-grid-item-wrap') ) {
										cachedItems.push(this);
									}
								});
							}

							self.renderGalleryItems(cachedItems, cachedIds);
						}
					},
					error: function(xhr, desc) {
						console.log(desc);
					}
				});
			}

			renderGalleryItems( cachedItems, cachedIds ) {
				const settings       = this.elements.$container.data('settings'),
					gallery          = this.elements.$gallery,
					justifiedGallery = this.elements.$justifiedGallery,
					self             = this;

				this.elements.$container.find('.pp-gallery-load-more').removeClass( 'disabled pp-loading' );

				if ( cachedItems.length > 0 ) {
					let count = 1;
					let items = [];

					$(cachedItems).each(function() {
						let id = $(this).data('item-id');

						if ( -1 === $.inArray( id, cachedIds ) ) {
							if ( count <= parseInt( settings.per_page, 10 ) ) {
								cachedIds.push( id );
								items.push( this );
								count++;
							} else {
								return false;
							}
						}
					});

					if ( items.length > 0 ) {
						items = $(items);

						items.imagesLoaded( function() {
							if ( settings.layout !== 'justified' ) {
								gallery.append( items );

								if ( self.masonry ) {
									self.elements.$container.imagesLoaded( function () {
										self.masonry.layout();
									} );
								}
							}

							if ( settings.layout === 'justified' ) {
								if ( justifiedGallery.length > 0 ) {
									gallery.append( items.fadeIn() );
									justifiedGallery.imagesLoaded( function() {
									})
									.done(function(instance) {
										setTimeout(function(){
											justifiedGallery.justifiedGallery( 'norewind' );
										}, 200 );

									});
								}
							}

							if ( settings.tilt_enable === 'yes' ) {
								self.initTilt();
							}

							self.initLightbox();
						} );
					}

					if ( cachedItems.length === cachedIds.length ) {
						this.elements.$container.find('.pp-gallery-pagination').hide();
					}
				}
			}

			hashChange() {
				setTimeout(function() {
					if ( location.hash && $(location.hash).length > 0 ) {
						if ( $(location.hash).parent().hasClass('pp-gallery-filters') ) {
							$(location.hash).trigger('click');
						}
					}
				}, 500);
			}

			initLightbox() {
				const galleryId      = this.elements.$gallery.attr( 'id' ),
					lightboxSelector = '.pp-grid-item-wrap .pp-image-gallery-item-link[data-fancybox="' + galleryId + '"]',
					fancyboxSettings = this.elements.$gallery.data('fancybox-settings');

				if ( $(lightboxSelector).length > 0 ) {
					$(lightboxSelector).fancybox( fancyboxSettings );
				}
			}

			initTilt() {
				const settings = this.elements.$container.data('settings'),
					gallery    = this.elements.$gallery;

				$(gallery).find('.pp-image-gallery-thumbnail-wrap').tilt({
					disableAxis: settings.tilt_axis,
					maxTilt: settings.tilt_amount,
					scale: settings.tilt_scale,
					speed: settings.tilt_speed,
					perspective: 1000
				});
			}

			onElementChange() {
				// Live relayout in the editor when controls change.
				if ( this.masonry ) {
					setTimeout( () => this.masonry.layout() );
				}
			}

			onDestroy() {
				if ( this.masonry ) {
					this.masonry.destroy();
				}

				if ( super.onDestroy ) {
					super.onDestroy();
				}
			}
		}

		elementorFrontend.elementsHandler.attachHandler( 'pp-image-gallery', ImageGalleryWidget );
	} );
})(jQuery);
