[Back]
import $ from 'jquery';
import rafSchd from 'raf-schd';
import { throttle } from 'throttle-debounce';

const { VPData } = window;
const { __ } = VPData;
const $wnd = $(window);

/**
 * Emit Resize Event.
 */
function windowResizeEmit() {
	if (typeof window.Event === 'function') {
		// modern browsers
		window.dispatchEvent(new window.Event('resize'));
	} else {
		// for IE and other old browsers
		// causes deprecation warning on modern browsers
		const evt = window.document.createEvent('UIEvents');
		evt.initUIEvent('resize', true, false, window, 0);
		window.dispatchEvent(evt);
	}
}

const visibilityData = {};
let shouldCheckVisibility = false;
let checkVisibilityTimeout = false;
let isFocusVisible = false;

// fix portfolio inside Tabs and Accordions
// check visibility by timer https://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom/33456469
//
// https://github.com/nk-crew/visual-portfolio/issues/11
// https://github.com/nk-crew/visual-portfolio/issues/113
function checkVisibility() {
	clearTimeout(checkVisibilityTimeout);

	if (!shouldCheckVisibility) {
		return;
	}

	const $items = $('.vp-portfolio__ready');

	if ($items.length) {
		let isVisibilityChanged = false;

		$items.each(function () {
			const { vpf } = this;

			if (!vpf) {
				return;
			}

			const currentState = visibilityData[vpf.uid] || 'none';

			visibilityData[vpf.uid] =
				this.offsetParent === null ? 'hidden' : 'visible';

			// changed from hidden to visible.
			if (
				currentState === 'hidden' &&
				visibilityData[vpf.uid] === 'visible'
			) {
				isVisibilityChanged = true;
			}
		});

		// resize, if visibility changed.
		if (isVisibilityChanged) {
			windowResizeEmit();
		}
	} else {
		shouldCheckVisibility = false;
	}

	// run again.
	checkVisibilityTimeout = setTimeout(checkVisibility, 500);
}

// run check function only after portfolio inited.
$(document).on('inited.vpf', (event) => {
	if (event.namespace !== 'vpf') {
		return;
	}

	shouldCheckVisibility = true;

	checkVisibility();
});

/**
 * If the most recent user interaction was via the keyboard;
 * and the key press did not include a meta, alt/option, or control key;
 * then the modality is keyboard. Otherwise, the modality is not keyboard.
 */
document.addEventListener(
	'keydown',
	function (e) {
		if (e.metaKey || e.altKey || e.ctrlKey) {
			return;
		}

		isFocusVisible = true;
	},
	true
);

/**
 * If at any point a user clicks with a pointing device, ensure that we change
 * the modality away from keyboard.
 * This avoids the situation where a user presses a key on an already focused
 * element, and then clicks on a different element, focusing it with a
 * pointing device, while we still think we're in keyboard modality.
 */
document.addEventListener(
	'mousedown',
	() => {
		isFocusVisible = false;
	},
	true
);
document.addEventListener(
	'pointerdown',
	() => {
		isFocusVisible = false;
	},
	true
);
document.addEventListener(
	'touchstart',
	() => {
		isFocusVisible = false;
	},
	true
);

/**
 * Main VP class
 */
class VP {
	constructor($item, userOptions) {
		const self = this;

		self.$item = $item;

		// get id from class
		const classes = $item[0].className.split(/\s+/);
		for (let k = 0; k < classes.length; k += 1) {
			if (classes[k] && /^vp-uid-/.test(classes[k])) {
				self.uid = classes[k].replace(/^vp-uid-/, '');
			}
			if (classes[k] && /^vp-id-/.test(classes[k])) {
				self.id = classes[k].replace(/^vp-id-/, '');
			}
		}
		if (!self.uid) {
			// eslint-disable-next-line no-console
			console.error(__.couldnt_retrieve_vp);
			return;
		}

		self.href = window.location.href;

		self.$items_wrap = $item.find('.vp-portfolio__items');
		self.$slider_thumbnails_wrap = $item.find('.vp-portfolio__thumbnails');
		self.$pagination = $item.find('.vp-portfolio__pagination-wrap');
		self.$filter = $item.find('.vp-portfolio__filter-wrap');
		self.$sort = $item.find('.vp-portfolio__sort-wrap');

		// find single filter block.
		if (self.id) {
			self.$filter = self.$filter.add(
				`.vp-single-filter.vp-id-${self.id} .vp-portfolio__filter-wrap`
			);
		}

		// find single sort block.
		if (self.id) {
			self.$sort = self.$sort.add(
				`.vp-single-sort.vp-id-${self.id} .vp-portfolio__sort-wrap`
			);
		}

		// user options
		self.userOptions = userOptions;

		self.firstRun = true;

		self.init();
	}

	// emit event
	// Example:
	// $(document).on('init.vpf', function (event, infiniteObject) {
	//     console.log(infiniteObject);
	// });
	emitEvent(event, data) {
		data = data ? [this].concat(data) : [this];
		this.$item.trigger(`${event}.vpf`, data);
		this.$item.trigger(`${event}.vpf-uid-${this.uid}`, data);
	}

	/**
	 * Init
	 */
	init() {
		const self = this;

		// destroy if already inited
		if (!self.firstRun) {
			self.destroy();
		}

		self.destroyed = false;

		self.$item.addClass('vp-portfolio__ready');

		// init options
		self.initOptions();

		// init events
		self.initEvents();

		// init layout
		self.initLayout();

		// init custom colors
		self.initCustomColors();

		self.emitEvent('init');

		if (self.id) {
			$(`.vp-single-filter.vp-id-${self.id}`)
				.addClass('vp-single-filter__ready')
				.parent('.vp-portfolio__layout-elements')
				.addClass('vp-portfolio__layout-elements__ready');
			$(`.vp-single-sort.vp-id-${self.id}`)
				.addClass('vp-single-sort__ready')
				.parent('.vp-portfolio__layout-elements')
				.addClass('vp-portfolio__layout-elements__ready');
		}

		// resized
		self.resized();

		// images loaded
		self.imagesLoaded();

		self.emitEvent('inited');

		self.firstRun = false;
	}

	/**
	 * Check if script loaded in preview.
	 *
	 * @return {boolean} is in preview.
	 */
	isPreview() {
		const self = this;

		return !!self.$item.closest('#vp_preview').length;
	}

	/**
	 * Called after resized container.
	 */
	resized() {
		windowResizeEmit();

		this.emitEvent('resized');
	}

	/**
	 * Images loaded.
	 */
	imagesLoaded() {
		const self = this;

		if (!self.$items_wrap.imagesLoaded) {
			return;
		}

		self.$items_wrap.imagesLoaded().progress(() => {
			this.emitEvent('imagesLoaded');
		});
	}

	/**
	 * Destroy
	 */
	destroy() {
		const self = this;

		// remove loaded class
		self.$item.removeClass('vp-portfolio__ready');

		if (self.id) {
			$(`.vp-single-filter.vp-id-${self.id}`)
				.removeClass('vp-single-filter__ready')
				.parent('.vp-portfolio__layout-elements')
				.removeClass('vp-portfolio__layout-elements__ready');
			$(`.vp-single-sort.vp-id-${self.id}`)
				.removeClass('vp-single-sort__ready')
				.parent('.vp-portfolio__layout-elements')
				.removeClass('vp-portfolio__layout-elements__ready');
		}

		// destroy events
		self.destroyEvents();

		// remove all generated styles
		self.removeStyle();
		self.renderStyle();

		self.emitEvent('destroy');

		self.destroyed = true;
	}

	/**
	 * Add style to the current portfolio list
	 *
	 * @param {string} selector css selector
	 * @param {string} styles   object with styles
	 * @param {string} media    string with media query
	 */
	addStyle(selector, styles, media) {
		media = media || '';

		const self = this;
		const { uid } = self;

		if (!self.stylesList) {
			self.stylesList = {};
		}

		if (typeof self.stylesList[uid] === 'undefined') {
			self.stylesList[uid] = {};
		}
		if (typeof self.stylesList[uid][media] === 'undefined') {
			self.stylesList[uid][media] = {};
		}
		if (typeof self.stylesList[uid][media][selector] === 'undefined') {
			self.stylesList[uid][media][selector] = {};
		}

		self.stylesList[uid][media][selector] = $.extend(
			self.stylesList[uid][media][selector],
			styles
		);

		self.emitEvent('addStyle', [selector, styles, media, self.stylesList]);
	}

	/**
	 * Remove style from the current portfolio list
	 *
	 * @param {string} selector css selector (if not set - removed all styles)
	 * @param {string} styles   object with styles
	 * @param {string} media    string with media query
	 */
	removeStyle(selector, styles, media) {
		media = media || '';

		const self = this;
		const { uid } = self;

		if (!self.stylesList) {
			self.stylesList = {};
		}

		if (typeof self.stylesList[uid] !== 'undefined' && !selector) {
			self.stylesList[uid] = {};
		}

		if (
			typeof self.stylesList[uid] !== 'undefined' &&
			typeof self.stylesList[uid][media] !== 'undefined' &&
			typeof self.stylesList[uid][media][selector] !== 'undefined' &&
			selector
		) {
			delete self.stylesList[uid][media][selector];
		}

		self.emitEvent('removeStyle', [selector, styles, self.stylesList]);
	}

	/**
	 * Render style for the current portfolio list
	 */
	renderStyle() {
		const self = this;

		// timeout for the case, when styles added one by one
		const { uid } = self;
		let stylesString = '';

		if (!self.stylesList) {
			self.stylesList = {};
		}

		// create string with styles
		if (typeof self.stylesList[uid] !== 'undefined') {
			Object.keys(self.stylesList[uid]).forEach((m) => {
				// media
				if (m) {
					stylesString += `@media ${m} {`;
				}
				Object.keys(self.stylesList[uid][m]).forEach((s) => {
					// selector
					const selectorParent = `.vp-uid-${uid}`;
					let selector = `${selectorParent} ${s}`;

					// add parent selector after `,`.
					selector = selector.replace(
						/, |,/g,
						`, ${selectorParent} `
					);

					stylesString += `${selector} {`;
					Object.keys(self.stylesList[uid][m][s]).forEach((p) => {
						// property and value
						stylesString += `${p}:${self.stylesList[uid][m][s][p]};`;
					});
					stylesString += '}';
				});
				// media
				if (m) {
					stylesString += '}';
				}
			});
		}

		// add in style tag
		let $style = $(`#vp-style-${uid}`);
		if (!$style.length) {
			$style = $('<style>')
				.attr('id', `vp-style-${uid}`)
				.appendTo('head');
		}
		$style.html(stylesString);

		self.emitEvent('renderStyle', [stylesString, self.stylesList, $style]);
	}

	/**
	 * First char to lower case
	 *
	 * @param {string} str string to transform
	 * @return {string} result string
	 */
	firstToLowerCase(str) {
		return str.substr(0, 1).toLowerCase() + str.substr(1);
	}

	/**
	 * Init options
	 *
	 * @param {Object} userOptions user options
	 */
	initOptions(userOptions) {
		const self = this;

		// default options
		self.defaults = {
			layout: 'tile',
			itemsGap: 0,
			pagination: 'load-more',
		};

		// new user options
		if (userOptions) {
			self.userOptions = userOptions;
		}

		// prepare data options
		const dataOptions = self.$item[0].dataset;
		const pureDataOptions = {};
		Object.keys(dataOptions).forEach((k) => {
			if (k && k.substring(0, 2) === 'vp') {
				pureDataOptions[self.firstToLowerCase(k.substring(2))] =
					dataOptions[k];
			}
		});

		self.options = $.extend(
			{},
			self.defaults,
			pureDataOptions,
			self.userOptions
		);

		self.emitEvent('initOptions');
	}

	/**
	 * Init events
	 */
	initEvents() {
		const self = this;
		const evp = `.vpf-uid-${self.uid}`;

		// Stretch
		function stretch() {
			const rect = self.$item[0].getBoundingClientRect();
			const { left } = rect;
			const right = window.innerWidth - rect.right;

			const ml = parseFloat(self.$item.css('margin-left') || 0);
			const mr = parseFloat(self.$item.css('margin-right') || 0);
			self.$item.css({
				marginLeft: ml - left,
				marginRight: mr - right,
				maxWidth: 'none',
				width: 'auto',
			});
		}
		if (self.$item.hasClass('vp-portfolio__stretch') && !self.isPreview()) {
			$wnd.on(`load${evp} resize${evp} orientationchange${evp}`, () => {
				stretch();
			});
			stretch();
		}

		// add helper focus class
		// TODO: change to CSS :has() when will be widely available
		// @link https://caniuse.com/?search=%3Ahas
		self.$item.on(`focus${evp}`, '.vp-portfolio__item a', function () {
			const $item = $(this).closest('.vp-portfolio__item');

			$item.addClass('vp-portfolio__item-focus');

			if (isFocusVisible) {
				$item.addClass('vp-portfolio__item-focus-visible');
			}
		});
		self.$item.on(`blur${evp}`, '.vp-portfolio__item a', function () {
			$(this)
				.closest('.vp-portfolio__item')
				.removeClass(
					'vp-portfolio__item-focus vp-portfolio__item-focus-visible'
				);
		});

		// on filter click
		self.$filter.on(
			`click${evp}`,
			'.vp-filter .vp-filter__item a',
			function (e) {
				e.preventDefault();
				const $this = $(this);
				if (!self.loading) {
					$this
						.closest('.vp-filter__item')
						.addClass('vp-filter__item-active')
						.siblings()
						.removeClass('vp-filter__item-active');
				}
				self.loadNewItems($this.attr('href'), true);
			}
		);

		// on sort click
		self.$sort.on(`click${evp}`, '.vp-sort .vp-sort__item a', function (e) {
			e.preventDefault();
			const $this = $(this);
			if (!self.loading) {
				$this
					.closest('.vp-sort__item')
					.addClass('vp-sort__item-active')
					.siblings()
					.removeClass('vp-sort__item-active');
			}
			self.loadNewItems($this.attr('href'), true);
		});

		// on filter/sort select change
		self.$filter
			.add(self.$sort)
			.on(
				`change${evp}`,
				'.vp-filter select, .vp-sort select',
				function () {
					const $this = $(this);
					const value = $this.val();
					const $option = $this.find(`[value="${value}"]`);

					if ($option.length) {
						self.loadNewItems($option.attr('data-vp-url'), true);
					}
				}
			);

		// on pagination click
		self.$item.on(
			`click${evp}`,
			'.vp-pagination .vp-pagination__item a',
			function (e) {
				e.preventDefault();
				const $this = $(this);
				const $pagination = $this.closest('.vp-pagination');

				if (
					$pagination.hasClass('vp-pagination__no-more') &&
					self.options.pagination !== 'paged'
				) {
					return;
				}

				self.loadNewItems(
					$this.attr('href'),
					self.options.pagination === 'paged'
				);

				// Scroll to top
				if (
					self.options.pagination === 'paged' &&
					$pagination.hasClass('vp-pagination__scroll-top')
				) {
					const $adminBar = $('#wpadminbar');
					const currentTop =
						window.pageYOffset ||
						document.documentElement.scrollTop;
					let { top } = self.$item.offset();

					// Custom user offset.
					if ($pagination.attr('data-vp-pagination-scroll-top')) {
						top -=
							parseInt(
								$pagination.attr(
									'data-vp-pagination-scroll-top'
								),
								10
							) || 0;
					}

					// Admin bar offset.
					if (
						$adminBar.length &&
						$adminBar.css('position') === 'fixed'
					) {
						top -= $adminBar.outerHeight();
					}

					// Limit max offset.
					top = Math.max(0, top);

					if (currentTop > top) {
						window.scrollTo({
							top,
							behavior: 'smooth',
						});
					}
				}
			}
		);

		// on categories of item click
		self.$item.on(
			`click${evp}`,
			'.vp-portfolio__items .vp-portfolio__item-meta-category a',
			function (e) {
				e.preventDefault();
				e.stopPropagation();
				self.loadNewItems($(this).attr('href'), true);
			}
		);

		// resized container
		self.$item.on(`transitionend${evp}`, '.vp-portfolio__items', (e) => {
			if (e.currentTarget === e.target) {
				self.resized();
			}
		});

		self.emitEvent('initEvents');
	}

	/**
	 * Destroy events
	 */
	destroyEvents() {
		const self = this;
		const evp = `.vpf-uid-${self.uid}`;

		// destroy click events
		self.$item.off(evp);
		self.$filter.off(evp);
		self.$sort.off(evp);

		// destroy window events
		$wnd.off(evp);

		self.emitEvent('destroyEvents');
	}

	/**
	 * Init layout
	 */
	initLayout() {
		const self = this;

		self.emitEvent('initLayout');

		self.renderStyle();
	}

	/**
	 * Init custom color by data attributes:
	 * data-vp-bg-color
	 * data-vp-text-color
	 */
	initCustomColors() {
		const self = this;

		self.$item.find('[data-vp-bg-color]').each(function () {
			const val = $(this).attr('data-vp-bg-color');
			self.addStyle(`[data-vp-bg-color="${val}"]`, {
				'background-color': `${val} !important`,
			});
		});

		self.$item.find('[data-vp-text-color]').each(function () {
			const val = $(this).attr('data-vp-text-color');
			self.addStyle(`[data-vp-text-color="${val}"]`, {
				color: `${val} !important`,
			});
		});

		self.renderStyle();

		self.emitEvent('initCustomColors');
	}

	/**
	 * Add New Items
	 *
	 * @param {object|dom|jQuery} $items         - elements.
	 * @param {bool}              removeExisting - remove existing elements.
	 * @param {Object}            $newVP         - new visual portfolio jQuery.
	 */
	addItems($items, removeExisting, $newVP) {
		const self = this;

		self.emitEvent('addItems', [$items, removeExisting, $newVP]);
	}

	/**
	 * Remove Items
	 *
	 * @param {object|dom|jQuery} $items - elements.
	 */
	removeItems($items) {
		const self = this;

		self.emitEvent('removeItems', [$items]);
	}

	/**
	 * AJAX Load New Items
	 *
	 * @param {string}   url            - url to request.
	 * @param {bool}     removeExisting - remove existing elements.
	 * @param {Function} cb             - callback.
	 */
	loadNewItems(url, removeExisting, cb) {
		const self = this;
		const { randomSeed } = self.options;

		if (
			(self.loading && typeof self.loading.readyState === 'undefined') ||
			!url ||
			self.href === url
		) {
			return;
		}

		// Abort previous AJAX loader to prevent conflict.
		// We need it mostly for Search feature, because users can type also when already in loading state.
		if (self.loading && self.loading.readyState && self.loading.abort) {
			self.loading.abort();
		}

		const ajaxData = {
			method: 'POST',
			url,
			data: {
				vpf_ajax_call: true,
				vpf_random_seed:
					typeof randomSeed !== 'undefined' ? randomSeed : false,
			},
			complete({ responseText }) {
				self.href = url;
				self.replaceItems(responseText, removeExisting, cb);
			},
		};

		self.loading = true;

		self.$item.addClass('vp-portfolio__loading');

		self.emitEvent('startLoadingNewItems', [url, ajaxData]);

		self.loading = $.ajax(ajaxData);
	}

	/**
	 * Replace items to the new loaded using AJAX
	 *
	 * @param {string}   content        - new page content.
	 * @param {bool}     removeExisting - remove existing elements.
	 * @param {Function} cb             - callback.
	 */
	replaceItems(content, removeExisting, cb) {
		const self = this;

		if (!content) {
			return;
		}

		// load to invisible container, then append to posts container
		content = content
			.replace('<body', '<body><div id="vp-ajax-load-body"')
			.replace('</body>', '</div></body>');
		const $body = $(content).filter('#vp-ajax-load-body');

		// find current block on new page
		const $newVP = $body.find(`.vp-portfolio.vp-uid-${self.uid}`);

		// insert new items
		if ($newVP.length) {
			const newItems = $newVP.find('.vp-portfolio__items').html();
			const nothingFound = $newVP.hasClass('vp-portfolio-not-found');

			// We should clean up notices here, as they may be cloned over and over.
			self.$item.find('.vp-notice').remove();

			if (nothingFound) {
				self.$item
					.find('.vp-portfolio__items-wrap')
					.before($newVP.find('.vp-notice').clone());
				self.$item.addClass('vp-portfolio-not-found');
			} else {
				self.$item.removeClass('vp-portfolio-not-found');
			}

			// update filter
			if (self.$filter.length) {
				self.$filter.each(function () {
					const $filter = $(this);
					let newFilterContent = '';

					if ($filter.parent().hasClass('vp-single-filter')) {
						newFilterContent = $body
							.find(
								`[class="${$filter
									.parent()
									.attr('class')
									.replace(
										' vp-single-filter__ready',
										''
									)}"] .vp-portfolio__filter-wrap`
							)
							.html();
					} else {
						newFilterContent = $newVP
							.find('.vp-portfolio__filter-wrap')
							.html();
					}

					$filter.html(newFilterContent);
				});
			}

			// update sort
			if (self.$sort.length) {
				self.$sort.each(function () {
					const $sort = $(this);
					let newFilterContent = '';

					if ($sort.parent().hasClass('vp-single-sort')) {
						newFilterContent = $body
							.find(
								`[class="${$sort
									.parent()
									.attr('class')
									.replace(
										' vp-single-sort__ready',
										''
									)}"] .vp-portfolio__sort-wrap`
							)
							.html();
					} else {
						newFilterContent = $newVP
							.find('.vp-portfolio__sort-wrap')
							.html();
					}

					$sort.html(newFilterContent);
				});
			}

			// update pagination
			if (self.$pagination.length) {
				self.$pagination.html(
					$newVP.find('.vp-portfolio__pagination-wrap').html()
				);
			}

			self.addItems($(newItems), removeExisting, $newVP);

			self.emitEvent('loadedNewItems', [$newVP, removeExisting, content]);

			if (cb) {
				cb();
			}
		}

		// update next page data
		const nextPageUrl = $newVP.attr('data-vp-next-page-url');
		self.options.nextPageUrl = nextPageUrl;
		self.$item.attr('data-vp-next-page-url', nextPageUrl);

		self.$item.removeClass('vp-portfolio__loading');

		self.loading = false;

		self.emitEvent('endLoadingNewItems');

		// images loaded
		self.imagesLoaded();

		// init custom colors
		self.initCustomColors();
	}
}

// extend VP object.
$(document).trigger('extendClass.vpf', [VP]);

// global definition
const plugin = function (options, ...args) {
	let ret;

	this.each(function () {
		if (typeof ret !== 'undefined') {
			return;
		}

		if (typeof options === 'object' || typeof options === 'undefined') {
			if (!this.vpf) {
				this.vpf = new VP($(this), options);
			}
		} else if (this.vpf) {
			ret = this.vpf[options](...args);
		}
	});

	return typeof ret !== 'undefined' ? ret : this;
};
plugin.constructor = VP;

// no conflict
const oldPlugin = $.fn.vpf;
$.fn.vpf = plugin;
$.fn.vpf.noConflict = function () {
	$.fn.vpf = oldPlugin;
	return this;
};

// initialization
$(() => {
	$('.vp-portfolio').vpf();
});

const throttledInit = throttle(
	200,
	rafSchd(() => {
		$('.vp-portfolio:not(.vp-portfolio__ready)').vpf();
	})
);
if (window.MutationObserver) {
	new window.MutationObserver(throttledInit).observe(
		document.documentElement,
		{
			childList: true,
			subtree: true,
		}
	);
} else {
	$(document).on('DOMContentLoaded DOMNodeInserted load', () => {
		throttledInit();
	});
}