Source: api/filter-api.js

import jQ from 'jquery';

import Globals from '../helpers/globals';
import Settings from '../helpers/settings';
import Utils from '../helpers/utils';
import Api from '../helpers/api';
import Navigation from '../helpers/navigation';
import BoostPFS from '../boost-pfs';
import FilterOptionEnum from "../enum/filter-option-enum";

var filterCallback = null;
var apiEvent = {
	eventType: '',
	eventInfo: {}
}
/**
 * The JSONP callback function for our API.
 * @param result
 */
let BoostPFSFilterCallback = (result) => {
	FilterApi.setDefaultValueForExcludedFields(result);

	if (typeof FilterApi.afterCall == 'function') {
		FilterApi.afterCall(result, apiEvent.eventType, apiEvent.eventInfo);
	}

	if (typeof FilterApi.afterCallAsync == 'function') {
		FilterApi.afterCallAsync(result, callbackFilterApi, apiEvent.eventType, apiEvent.eventInfo);
		return;
	}

	callbackFilterApi(result);
};

/**
 * Calls API with JSONP.
 * @param {string} eventType
 * @param {function} successCallback
 * @param {function} errorCallback
 */
const getFilterData = (eventType, successCallback, errorCallback) => {
	// Prepare request param
	prepareRequestParams(eventType);

	// Execute all before get filter data functions (internal use)
	if (beforeFilterApplyCallback) {
		beforeFilterApplyCallback.forEach(func => {
			if (typeof func == 'function') {
				func(eventType);
			}
		});
	}

	// Execute before get filter data (customization use)
	if (typeof FilterApi.beforeCall == 'function') {
		FilterApi.beforeCall(eventType, apiEvent.eventInfo);
	}

	// Execute before get filter data (customization use - call async 3rd party)
	if (typeof FilterApi.beforeCallAsync == 'function') {
		var callFilterApiWrapper = () => {
			callFilterApi(eventType, successCallback, errorCallback);
		}
		FilterApi.beforeCallAsync(callFilterApiWrapper, eventType, apiEvent.eventInfo);
		return;
	}

	callFilterApi(eventType, successCallback, errorCallback);
};

/**
 * Calls API with JSONP.
 * @param {string} eventType
 * @param {function} successCallback
 * @param {function} errorCallback
 * @param {number} errorCount
 */
const callFilterApi = (eventType, successCallback, errorCallback, errorCount) => {
	// Get filter data
	errorCount = typeof errorCount !== 'undefined' ? errorCount : 0;
	filterCallback = successCallback;
	Globals.queryParams.callback = 'BoostPFSFilterCallback';
	Globals.queryParams.event_type = eventType;
	// Prepare url
	var url = Api.getApiUrl('filter');
	if (Utils.isSearchPage()) {
		url = Api.getApiUrl('search');
		if (Globals.hasOwnProperty('searchDisplay') && Globals.searchDisplay && Globals.searchDisplay !== 'products') {
			url += '/' + Globals.searchDisplay;
		}
	}
	
	// Create request
	var script = document.createElement("script");
	script.type = 'text/javascript';
	var timestamp = new Date().getTime();

	// Remove functions from query params
	var cleanQueryParams = JSON.parse(JSON.stringify(Globals.queryParams));
	Globals.queryParams = cleanQueryParams;

	script.src = url + '?t=' + timestamp + '&' + jQ.param(Globals.queryParams);
	script.id = 'boost-pfs-filter-script';
	script.async = true;
	var resendAPITimer, resendAPIDuration = 2000;
	script.addEventListener('error', function (e) {
		if (typeof document.getElementById(script.id).remove == 'function') {
			// If support is found
			document.getElementById(script.id).remove();
		} else {
			// If not
			document.getElementById(script.id).outerHTML = '';
		}
		// Resend API 2 times when got the error
		if (errorCount < 2) {
			errorCount++;
			if (resendAPITimer) {
				clearTimeout(resendAPITimer);
			}
			resendAPITimer = setTimeout(callFilterApi('resend', successCallback, errorCallback, errorCount), resendAPIDuration);
		} else {
			if (typeof errorCallback == 'function') {
				errorCallback();
			}
		}
	});
	// Append the script element to the HEAD section.
	document.getElementsByTagName('head')[0].appendChild(script);
	script.addEventListener('load', function (e) {
		if (typeof document.getElementById(script.id).remove == 'function') {
			// If support is found
			document.getElementById(script.id).remove();
		} else {
			// If not
			document.getElementById(script.id).outerHTML = '';
		}
	});
};

const callbackFilterApi = (result) => {
	if (typeof filterCallback == 'function') {
		filterCallback(result, apiEvent.eventType, apiEvent.eventInfo);
	}
}

// List function that will be called before get filter data
var beforeFilterApplyCallback = [];
const addBeforeApplyFilter = (func) => {
	beforeFilterApplyCallback.push(func);
}

/**
 * Prepare the Globals.queryParams before sending API request.
 * It merges Globals.queryParams
 * and extra params (page, limit, currency,...).
 * @param {string} eventType
 */
const prepareRequestParams = (eventType) => {
	var params = mergeObject({}, Globals.queryParams);
	// Get Filter params
	params = prepareFilterParams(params, eventType);
	// Search Page: Get Search params
	params = prepareSearchParams(params, eventType);
	// Revert price in queryParams to default currency (Multi-currency)
	params = revertPriceParams(params, eventType);
	// Set locale param
	params = Api.setApiLocaleParams(params);
	// Remove currency param
	if (params.hasOwnProperty('currency')) {
		delete params.currency;
	}
	// Remove currency rate param
	if (params.hasOwnProperty('currency_rate')) {
		delete params.currency_rate;
	}
	// Set data back to queryParams
	Globals.queryParams = params;
};

/**
 * Revert price in queryParams to default currency (Multi-currency)
 * @param {Object} params - The query params
 */
const revertPriceParams = (params, eventType) => {
	// Detect price filter param by key prefix (pf_p_ or pf_vp_)
	var paramKeys = Object.keys(params);
	paramKeys = paramKeys.filter(function(key) {
		return (key.indexOf('pf_p_') == 0 || key.indexOf('pf_vp_') == 0) && !key.includes('_exclude_from_value');
	});
	// Storaged the price of active currency
	Globals.activeCurrencyPrices = [];
	if (paramKeys.length) {
		paramKeys.forEach((key) => {
			var priceValueList = [];
			if (Array.isArray(params[key])) {
				params[key].forEach(paramValue => {
					var priceVal = paramValue.split(':');
					priceVal = priceVal.map((val, i) => {
						var isMin = i == 0? true: false;
						return val.length == 0? '': Utils.revertPriceToDefaultCurrency(val, isMin);
					});
					priceValueList.push(priceVal.join(':'));
				})
			}
			Globals.activeCurrencyPrices[key] = params[key];
			params[key] = priceValueList;
		});
	}
	return params;
}

/**
 * Build the params object for Filter API.
 * @param {Object} params
 * @param {string} eventType
 * @return {Object}
 */
const prepareFilterParams = (params, eventType) => {
	// History state
	var historyState = Navigation.getHistoryState();

	// Get collection id
	// If clicking back button, read from history
	var collectionId = 0;
	if (eventType == 'history') {

		// If history has collection_scope (back button to non-first load)
		if (historyState && historyState.param && historyState.param.hasOwnProperty('collection_scope')) {
			collectionId = parseInt(historyState.param.collection_scope);
		// If history doesnt have collection_scope, read from liquid (back button to first load)
		} else {
			collectionId = parseInt(boostPFSConfig.general.collection_id);
		}

	// If not back button, read from setting
	} else {
		// Get collection id from filter tree (non-first load)
		if (Globals.collectionId != null) {
			collectionId = parseInt(Globals.collectionId);

		// Get collection id from liquid (first load)
		} else if (boostPFSConfig.general.collection_id) {
			collectionId = parseInt(boostPFSConfig.general.collection_id);
		}
	}
	// Assign collection id
	Globals.collectionId = collectionId;
	params.collection_scope = collectionId;

	// Get tag value
	// If clicking back button, read from history
	var collectionTags = null;
	if (eventType == 'history') {
		// If history has 'tag', read from history (back button to non-first load)
		if (historyState && historyState.param && historyState.param.hasOwnProperty('tag')) {
			collectionTags = historyState.param.tag;
		// If history doesnt have 'tag', read from liquid (back button to first load)
		} else {
			collectionTags = boostPFSConfig.general.collectionTags;
		}

	// If not back button, read from setting
	} else {
		// Get collection tags from filter tree (non-first load)
		if (Globals.collectionTags) {
			collectionTags = Globals.collectionTags;

		// Get collection tags from liquid (first load)
		} else if (boostPFSConfig.general.collectionTags) {
			collectionTags = boostPFSConfig.general.collectionTags;
		}
	}
	Globals.collectionTags = collectionTags;
	params.tag = collectionTags;

	// Get product/variant available or not
	if (Settings.getSettingValue('general.availableAfterFiltering') == true) {
		params.product_available = Utils.checkExistFilterOptionParam() === true ? true : Globals.productAvailable;
		params.variant_available = Utils.checkExistFilterOptionParam() === true ? true : Globals.variantAvailable;
	} else {
		params.product_available = Globals.productAvailable;
		params.variant_available = Globals.variantAvailable;
	}
	// Display filter option items even they return no product
	if (Settings.getSettingValue('general.showOutOfStockOption')) params.zero_options = true;
	// Build Filter tree or not
	params.build_filter_tree = (typeof eventType !== 'undefined' && Globals.imutableFilterTree.indexOf(eventType) > -1) ? false : true; // Except events of elements in filter tree, all remaining events do not rebuild filter tree
	// Enable cache when request params doest not have filter option values, "page" param equal to 1 and "sort" param equal to default sorting
	params.check_cache = (Utils.checkExistFilterOptionParam() === false && params.page == 1 && params.sort == Globals.defaultSorting && params.limit == Settings.getSettingValue('general.limit') && !Utils.isSearchPage() && !Utils.isVendorPage() && !Utils.isTypePage()) ? true : false;
	// Turn on price mode
	if (Settings.getSettingValue('general.priceMode') != '') params.price_mode = Settings.getSettingValue('general.priceMode');
	// Tag mode
	if (Settings.getSettingValue('general.tagMode') != '') params['tag_mode'] = Settings.getSettingValue('general.tagMode');
	// Availability Sorting
	if (Settings.getSettingValue('general.sortingAvailableFirst')) params.sort_first = 'available';
	// Vendor page: Get Vendor param
	if (Utils.isVendorPage() && params.hasOwnProperty('q')) {
		var vendorParam = Settings.getSettingValue('general.vendorParam');
		// params[vendorParam] = [params['q'].replace(/\+/g, ' ')];
		params[vendorParam] = [params['q']];
		delete params['q'];
	}
	// Type page: Get Type param
	if (Utils.isTypePage() && params.hasOwnProperty('q')) {
		var typeParam = Settings.getSettingValue('general.typeParam');
		params[typeParam] = [params['q']];
		delete params['q'];
	}
	return params;
};

/**
 * Build the params object for Search API.
 * @param {Object} params
 * @param {string} eventType
 * @return {Object}
 */
const prepareSearchParams = (params, eventType) => {
	if (Utils.isSearchPage()) {
		params['q'] = Utils.getSearchTerm();
		if (Globals.searchTermKey != 'q') delete params[Globals.searchTermKey];
		var enableFuzzy = Settings.getSettingValue('search.enableFuzzy');
		if (enableFuzzy !== true) params.fuzzy = enableFuzzy;
		if (Settings.getSettingValue('search.reduceMinMatch') !== false) {
			params.reduce_min_match = Settings.getSettingValue('search.reduceMinMatch');
		}
		if (Settings.getSettingValue('search.fullMinMatch')) params.full_min_match = true;
		if (Settings.getSettingValue('general.sortingAvailableFirst')) params.sort_first = 'available';
		if (Settings.getSettingValue('search.enablePlusCharacterSearch')) params.enable_plus_character_search = true;
	}
	return params;
};

/**
 * Update the Globals.queryParam object
 * from the current URL. This is called once on app load.
 * @param url
 */
const updateParamsFromUrl = (url) => {
	// Get Filter params
	var params = getFilterParams(url);
	// Set default params if missing from url params
	params = setDefaultParams(params);
	// Set data back to queryParams
	Globals.queryParams = params;
};

/**
 * Update the Globals.queryParam object
 * from the current URL. This is called once on app load.
 * @param url
 */
const getFilterParams = (url) => {
	var urlQueryString = '';
	if (!url) {
		urlQueryString = Utils.getWindowLocation().search;
	} else {
		urlQueryString = typeof url == 'string' && url.split('?').length == 2 ? url.split('?')[1] : '';
	}

	var urlSearchParams = new URLSearchParams(urlQueryString);
	var queryParams = {};

	var urlScheme = Settings.getSettingValue('general.urlScheme');

	urlSearchParams.forEach(function(value, key) {
		var longKey = Navigation.longParamMap.get(key);
		if (!longKey) longKey = key;

		var isSpecialeKey = Globals.imutableFilterTree.includes(longKey)
			|| longKey == Globals.searchTermKey
			|| (longKey.startsWith(Globals.prefix) && longKey.includes('_and_condition'))
			|| (longKey.startsWith(Globals.prefix) && longKey.includes('_show_exact_rating'))
			|| (longKey.startsWith(Globals.prefix) && longKey.includes('_exclude_from_value'));

		var isFilterOptionKey = longKey.startsWith(Globals.prefix);

		if (isSpecialeKey) {
			queryParams[longKey] = value;
			Globals.hasFilterOptionParam = true;
		} else if (isFilterOptionKey) {

			var isFilterCollectionKey = longKey.startsWith(Globals.prefix + '_c_');
			var isFilterCollectionTagKey = longKey.startsWith(Globals.prefix + '_ct_');

			// If filter by collection, set 'collection_scope'
			if (isFilterCollectionKey) {
				if (Utils.isSearchPage()) {
					queryParams.collection_scope = value;
				} else {
					return;
				}

			// If filter by collection tags, set 'tag'
			} else if (isFilterCollectionTagKey) {
				var tagMode = Settings.getSettingValue('general.multiLevelCollectionSelectType');
				queryParams['tag_mode'] = tagMode == FilterOptionEnum.SelectType.MULTIPLE ? '2' : '1';
				longKey = 'tag';
			}

			switch (urlScheme) {
				case 2:
					queryParams[longKey] = value.split(',');
					break;
				case 1:
				default:
					if (queryParams.hasOwnProperty(longKey)) {
						queryParams[longKey].push(value);
					} else {
						queryParams[longKey] = [value];
					}
					break;
			}

			// Set the global variables by query params
			if (isFilterCollectionKey) {
				Globals.collectionId = queryParams.collection_scope;
			} else if (isFilterCollectionTagKey) {
				Globals.collectionTags = queryParams.tag;
			}

			Globals.hasFilterOptionParam = true;
		}
	});

	return queryParams;
};

const setDefaultParams = (params) => {
	params['_'] = Globals.prefix;
	params.shop = params.hasOwnProperty('shop') ? params.shop : Globals.shopDomain;
	params.page = params.hasOwnProperty('page') ? parseInt(params.page) : 1;

	// Products per page
	var settingLimit = Settings.getSettingValue('general.limit');
	if (Settings.getSettingValue('general.paginationType') == 'default' || Settings.getSettingValue('general.paginationTypeAdvanced')) {
		params.limit = params.hasOwnProperty('limit') ? params.limit : settingLimit;
	} else {
		params.limit = (params.hasOwnProperty('limit') ? params.limit : settingLimit) * params.page;
	}
	// Sorting
	if (Utils.isSearchPage()) Globals.defaultSorting = 'relevance';
	params.sort = params.hasOwnProperty('sort') ? params.sort : Globals.defaultSorting;
	// Display type
	params.display = params.hasOwnProperty('display') ? params.display : Settings.getSettingValue('general.defaultDisplay');

	// Prepare the params for the third-party app
	params = setThirdPartyAppParams(params);

	return params;
};

const setThirdPartyAppParams = (params) => {
	params = setShopifyMultiCurrencyParams(params);
	return params;
};

const setShopifyMultiCurrencyParams = (params) => {
	// If this store has the multi-currency feature
	if (typeof boostPFSConfig !== 'undefined' &&
		typeof boostPFSConfig.general.currencies != 'undefined' &&
		boostPFSConfig.general.currencies.length > 1) {
		// Get the current currency code
		var currentCurrency = boostPFSConfig.general.current_currency.toLowerCase().trim();
		//  Pass the currency param to our api
		params.currency = currentCurrency;
		// Pass the currency_rate to our api
		if (typeof Shopify !== 'undefined' &&
			typeof Shopify.currency !== 'undefined' &&
			typeof Shopify.currency.rate !== 'undefined') {
			params.currency_rate = Shopify.currency.rate;
		}
	}
	return params;
};

const setParam = (key, value) => {
	// Set query params
	if (value === null || typeof value == 'undefined' || (Array.isArray(value) && value.length == 0)) {
		delete Globals.queryParams[key];
	} else {
		if (Array.isArray(value)) {
			// Removes duplicate values
			Globals.queryParams[key] = [...new Set(value)];
		} else {
			Globals.queryParams[key] = value;
		}
	}
	// Set the global values by query params
	var isFilterCollectionKey = key.startsWith(Globals.prefix + '_c_');
	var isFilterCollectionTagKey = key.startsWith(Globals.prefix + '_ct_');
	var paramValue = Globals.queryParams[key];
	if (isFilterCollectionKey || key == 'collection_scope') {
		Globals.queryParams.collection_scope = paramValue;
		Globals.collectionId = paramValue;
		// If clear the collection, clear all the tags in it
		if (!paramValue) {
			var tagKey = key.replace(Globals.prefix + '_c_', Globals.prefix + '_ct_');
			delete Globals.queryParams[tagKey];
			Globals.queryParams.tag = null;
			Globals.collectionTags = null;
		}
	}
	if (isFilterCollectionTagKey) {
		Globals.queryParams.tag = paramValue;
		Globals.collectionTags = paramValue;
	}
}

const applyFilter = (eventType, eventInfo) => {
	apiEvent.eventType = eventType;
	apiEvent.eventInfo = eventInfo;
	BoostPFS.instance.filter.filterLoadingIcon.setShow(true);

	getFilterData(
		eventType,
		BoostPFS.instance.filter.setData.bind(BoostPFS.instance.filter),
		BoostPFS.instance.filter.errorFilterCallback.bind(BoostPFS.instance.filter)
	);
	Navigation.updateAddressBar();
}

/**
 * The API exclude some product fields in the response.
 * We set the default values for these field.
 * @param apiResult
 */
const setDefaultValueForExcludedFields = (apiResult) => {
	if (Array.isArray(apiResult.products)){
		var currentDate = new Date();
		var currentDateString = currentDate.toISOString();

		apiResult.products.forEach((product) => {
			if (!product.hasOwnProperty('variants')) {
				product.variants = [];
			}
			if (!product.hasOwnProperty('images_info')) {
				product.images_info = [];
			}
			if (!product.hasOwnProperty('collections')) {
				product.collections = [];
			}
			if (!product.hasOwnProperty('tags')) {
				product.tags = [];
			}
			if (!product.hasOwnProperty('skus')) {
				product.skus = [];
			}
			if (!product.hasOwnProperty('options_with_values')) {
				product.options_with_values = [];
			}
			if (!product.hasOwnProperty('barcodes')) {
				product.barcodes = [];
			}
			if (!product.hasOwnProperty('created_at')) {
				product.created_at = currentDateString;
			}
			if (!product.hasOwnProperty('updated_at')) {
				product.updated_at = currentDateString;
			}
			if (!product.hasOwnProperty('published_at')) {
				product.published_at = currentDateString;
			}
		})
	}
}

const FilterApi = {
	BoostPFSFilterCallback: BoostPFSFilterCallback,
	getFilterData: getFilterData,
	updateParamsFromUrl: updateParamsFromUrl,
	setParam: setParam,
	setDefaultValueForExcludedFields: setDefaultValueForExcludedFields,
	addBeforeApplyFilter: addBeforeApplyFilter,
	applyFilter: applyFilter,
	callFilterApi: callFilterApi,
	callbackFilterApi: callbackFilterApi,
	beforeCall: null,
	afterCall: null,
	beforeCallAsync: null,
	afterCallAsync: null
}

export default FilterApi;