// This is for transify.py to collect and prepare i18n data in parent HTML page.
if(window.i18njs)
{
	i18n.t('alert_follow_limit');
	i18n.t('alert_follow_too_frequent');
	i18n.t('msg_server_error');
}

var BJUtil = (function(){
	// Assumption: There is only one modal frame at the same time.
	// So if pop is not passed a framename, it will pop the latest frame.
	var latestFrameName = null;
	var viewWillReappearHandler_call_backs = [];
	var imgVersion = "_version=2";

	function init(sv) {
	}

	function isSimpleVersion() {
		return window.SIMPLE_VERSION;
	}

	function configurePageTitle(title) {
		if (isWeChat()) {
			// In order for WeChat to show the shop username in link preview.
			document.title = title + ' - ' + get_shopee_brand();
		} else {
			document.title = title;
		}
		var bridge = window.WebViewJavascriptBridge;
		if (bridge) {
			bridge.callHandler("configureNavBarTitle", { title: title });
		}
		$('.header .page_name').text(title);
		// set app page title
		// set menu bar title
	}

	// additionalConfig is the configs passed to bridgeHandler. It may not be used if
	// replaceCurrentPage is true.
	// By default, web will open url in current window, while app will open a new webview.
	// After pass inCurrentPage, webNewWindow or popHistory, the behaviour will be unified.
	// Cheat sheet:
	// 		navigate(url) === bridgeCallHandler('navigate', {url: url});
	// 		navigate(url, true) === location.href = url
	// 		navigate(url, true, null, null, true) === location.href.replace(url)
	function navigate(targetUrl, inCurrentPage, webNewWindow, additionalConfig, popHistory){
		var config = additionalConfig || {};

		if(inCurrentPage){
			config['inCurrentPage'] = true;
		}

		if(popHistory){
			config['popHistory'] = true;
		}

		if (webNewWindow) {
			config['target'] = '_blank';
		}

		if (targetUrl.startsWith('//')) {
			config['url'] = location.protocol + targetUrl;
		} else if (targetUrl.startsWith('/')) {
			config['url'] = location.origin + targetUrl;
		} else {
			config['url'] = targetUrl;
		}
		bridgeCallHandler('navigate', config, function(response) {});
	}
	function navigateModally(targetUrl, frame){
		if($('iframe[src="'+targetUrl+'"]').length > 0){
			return;
		}
		// for web only, slide up from bottom
		if (isAndroid()){
			var modal = $('<iframe/>',{
				class:'modal-frame-android ' + frame,
				src:targetUrl
			});
		}
		else {
			var modal = $('<div/>',{
				class:'modal-frame-holder-ios ' + frame
			});
			var modal_frame = $('<iframe/>',{
				class:'modal-frame-ios',
				src:targetUrl
			});
			modal_frame.appendTo(modal);
		}
		latestFrameName = frame;
		modal.appendTo('body');
		modal.delay(20).velocity({
			top: '0'
		},800,'easeInOutQuint');
		/* DO NOT put hidePage in delay/velocity, it might get blocked and cause trouble
		*  The animation will be blocked if new page pop up
		* */
		setTimeout(function() {
			hidePage();
		}, 820);
	}
	function pop(frame){
		if(!frame) {
			frame = latestFrameName;
		}
		$('.'+frame).velocity({
			top: '100%'
		},800,'easeInOutCubic',function(){
			$('.'+frame).remove();
		});
		latestFrameName = null; // only consider at most 1 frame overlay, so every time frame is popped, our stack is empty
		showPage();
	}
	function hidePage(){
		var $body = $('body');
		$('.page_hide').show();
		$body.attr('scrollTop', $body.scrollTop());
		$body.css('height', 0);
	}
	function showPage(){
		var $body = $('body');
		$body.css('height', '');
		$body.scrollTop($body.attr('scrollTop'));
		$body.attr('scrollTop', null);
		$('.page_hide').hide();
	}
	function viewWillReappearHandler(e){
		if(viewWillReappearHandler_call_backs.length > 0){
			for (var i=0; i<viewWillReappearHandler_call_backs.length; i++){
				viewWillReappearHandler_call_backs[i](e);
			}
		}
	}
	function viewWillReappear(call_back){
		viewWillReappearHandler_call_backs.push(call_back);
	}
	function ajax(url, data, call_back, type){
		if (type == null){
			type = 'GET';
		}
		if (typeof url == "string"){
			var ajax_data = {
				type: type,
				url: url,
				data: data
			};
		} else {
			var ajax_data = url;
			url = ajax_data['url'];
			data = ajax_data.data;
			type = ajax_data.type;
		}
		var promise = $.ajax(ajax_data)
		.done(function (responseData, status, xhr) {
			if (filterAjaxResponseHasError(responseData)){
				var log_data = {'url': url,
					'sent_data': JSON.stringify(data),
					'response_data': JSON.stringify(responseData)
				};
				sentryLog(url, log_data, "error");
			}
			if (call_back) {
				call_back(responseData);
			}
		})
		.fail(function (xhr, status, error) {
			// Do not report abort status because this happens when android PLV reuses a page, and it's normal.
			if(status == 'abort' || error == 'abort') return;

			if ('responseText' in xhr) {
				if (xhr.responseText.length > 200){
					xhr['responseText'] = xhr['responseText'].substr(0, 200)+'...';
				}
			}
			var log_data = {'url': url,
				'sent_data': JSON.stringify(data),
				'response_data': JSON.stringify({'xhr': xhr, 'status': status, 'error': error})
			};
			sentryLog(url, log_data, "fail");
		});
		return promise;
	}
	function filterAjaxResponseHasError(responseData){
		if (typeof responseData == "object"){
			var errors = ['err', 'error', 'err_code', 'error_code'];
			for ( var i=0; i<errors.length; i++ ) {
				var error = errors[i];
				if (error in responseData){
					if (responseData.error != 0){
						return true;
					}
				}
			}
		}
		return false;
	}
	function sentryLog(key, data, type){
		if (type == null){
			type = "error";
		}
		var url = "/func/log/" + type + "/?url=" + encodeURIComponent(key);
		data['csrfmiddlewaretoken'] = csrf;
		$.post(url, data);
	}
	function post(url, data, call_back){
		return ajax(url, data, call_back, 'POST');
	}
	function get(url, data, call_back){
		return ajax(url, data, call_back, 'GET');
	}

	/**
	 * @typedef {Object} PollingConfigType
	 * @property {Object} ajaxData -- ajax data to send
	 * @property {number} frequency -- polling frequency (ms)
	 * @property {number} timeout -- timeout (ms)
	 * @property {function} [continueCallback]
	 * @property {function} [finishCallback]
	 */

	/**
	 * @param {PollingConfigType} config
	 * @param {function} isFinal -- Criteria to check whether to stop polling (before timeout)
	 * @param {function} finishCallback -- ignore config.finishCallback if given
	 */
	function polling(config, isFinal, finishCallback) {
		var startTime = Date.now();
		var freq = config.frequency;
		var timeout = config.timeout;
		var intervId = setInterval(pollingAction, freq);

		var continueCallback = config.continueCallback || function(){};
		finishCallback = finishCallback || config.finishCallback || function(){};

		function isTimeout() {
			return Date.now() - startTime > timeout;
		}

		function pollingAction() {
			$.ajax(config.ajaxData).done(function () {
				if (isFinal.apply(null, arguments) || isTimeout()) {
					clearInterval(intervId);
					finishCallback.apply(null, arguments);
				} else {
					continueCallback.apply(null, arguments);
				}
			}).fail(function () {
				if (isTimeout()) {
					clearInterval(intervId);
					finishCallback({is_send_request_failed: true, data: arguments});
				} else {
					continueCallback({is_send_request_failed: true, data: arguments});
				}
			});
		}
		pollingAction();
	}

	function listenLike($like) {
		$like.on('tap', function(e){
			checkLogin(listenLikeAction, {$like: $like, e: e});
		});
	}
	function popUserPrivacy(){
		if (window['ALERT_LIKE_PRIVACY_MSG']){
			ALERT_LIKE_PRIVACY_MSG = false;
			$.get('/timeline/api/set_no_alert_privacy/', function(){
			});
			bridge = window.WebViewJavascriptBridge;
			if (bridge) {
				bridge.callHandler('showPopUp',{
					popUp:{
						message:i18n.t('label_your_activity_are_public'),
						buttonType:1,
						cancelText:i18n.t('label_change_setting'),
						okText:i18n.t('label_ok')
					}
				},function(e){
					if(e.buttonClicked == 1){
						bridge.callHandler("navigateAppPath",{'path':'privacySettings'},function(){
						})
					}
				});
			}
		}
	}
	function listenLikeAction(param){
		popUserPrivacy();
		var $like = param.$like;
		var e = param.e;
		var shopid = $like.data('shopid');
		var itemid = $like.data('itemid');
		var feedid = $like.data('feedid');
		var $number = $like.find('.number');
		var $icon = $like.find('.icon');
		var liked = $like.attr('data-liked');
		var isShop = itemid ? false : true;
		e.preventDefault();
		if (liked == 1){
			var cur_liked = isNaN(parseInt($number.text())) ? 0:parseInt($number.text());
			var unlike_url = isShop ? "/buyer/unlike/feed/" + feedid + '/' : "/buyer/unlike/shop/"+shopid+"/item/"+itemid+"/";
			$number.text(--cur_liked>0 ? cur_liked : i18n.t('label_like'));
			$like.attr('data-liked', 0);
			$icon.removeClass('ic_feed_liked').addClass('ic_feed_like');
			$.post(unlike_url, {"csrfmiddlewaretoken":csrf}).fail(function(e){
				alert_message(msg_server_error);
			});
		} else{
			var cur_liked = isNaN(parseInt($number.text())) ? 0:parseInt($number.text());
			var like_url = isShop ? "/buyer/like/feed/" + feedid + '/' : "/buyer/like/shop/"+shopid+"/item/"+itemid+"/";
			$number.text(++cur_liked);
			$like.attr('data-liked', 1);
			$icon.removeClass('ic_feed_like').addClass('ic_feed_liked');
			$.post(like_url, {"csrfmiddlewaretoken":csrf}).fail(function(e){
				alert_message(msg_server_error);
			});
		}
	}
	function listenFollow($follow, newwindow, aggressive, callback){
		// aggressive: apply the action on all the follow with same shopid & itemid on the page
		$follow.on('tap', function(e){
			e.preventDefault();
			e.stopPropagation();
			checkLogin(listenFollowAction, {$follow: $follow, newwindow: newwindow, e: e, aggressive: aggressive, callback: callback});
		});
	}
	function listenFollowAction(param){
		var $follow = param.$follow;
		var e = param.e;
		var newwindow = param.newwindow;
		var aggressive = param.aggressive;
		var callback = param.callback;
		e.stopPropagation();
		var shopid = $follow.data('shopid');
		var followed = $follow.attr('data-followed'); // somehow $follow.data('') will never update once set
		e.preventDefault();
		e.stopPropagation();
		if (followed == 1){
			/* old logic
			 * auto direct to shop
			 */
			// var url = '/shop/#shopid=' + shopid;
			// navigate(url, false, newwindow);
			if (aggressive){
				$follow = $(".follow[data-shopid="+shopid+"]");
				// tricky!! here callback is only used for unfollow all the same user, read logic carefully.
				if (callback) {
					callback($follow, shopid, false);
				}
			}
			$follow.attr('data-followed', 0);
			$follow.removeClass('followed');
			$follow.html($('<span>+</span> <span>'+i18n.t('label_follow')+'</span>'));
			$.post("/buyer/unfollow/shop/"+shopid+"/", {"csrfmiddlewaretoken":csrf}).fail(function(e){
				alert_message(msg_server_error);
			});
		} else{
			if (aggressive){
				$follow = $(".follow[data-shopid="+shopid+"]");
				// tricky!! here callback is only used for unfollow all the same user, read logic carefully.
				if (callback) {
					callback($follow, shopid, true);
				}
			}
			$follow.attr('data-followed', 1);
			$follow.addClass('followed');
			$follow.html($('<span>'+i18n.t('label_following')+'</span>'));
			$.post("/buyer/follow/shop/"+shopid+"/", {"csrfmiddlewaretoken":csrf}).fail(function(e){
				alert_message(msg_server_error);
			});
		}
	}
	function checkLogin(call_back, param){
		bridge = window.WebViewJavascriptBridge;
		if (bridge) {
			bridge.callHandler("login", {}, function(e){
				if (e.status == 1) {
					call_back(param);
				}
			});
		} else {
			if (loggedin) {
				call_back(param);
			}
			else {
				askLogin();
			}
		}
	}
	function findHash(text, hashtag_list){
		var rstr = 'wUfxQfUPzffdyrer';
		if (!hashtag_list){
			return text;
		}
		hashtag_list = hashtag_list.sort(function(a,b){ return b.length - a.length; });
		if (!hashtag_list || hashtag_list.length == 0) return text;
		for (var i=0; i<hashtag_list.length; i++){
			var hash = hashtag_list[i];
			var hash_rege = new RegExp(hash, 'gi');
			var matches = text.match(hash_rege);
			if (matches != null){
				for (var match in matches){
					if (!matches.hasOwnProperty(match)) continue;
					text = text.replace(matches[match], '<a class="hashtag">' + rstr + matches[match].substring(1)  + '</a>');
				}
			}
		}
		text = text.replace(new RegExp(rstr, 'gi'), '#');
		return text;
	}
	function listenHash($hashtag, newwindow, isImage, cb){
		$hashtag.on('tap', function(e){
			e.stopPropagation();
			var hashtag = isImage ? $(this).data('hash').toString() : $(this).text().substring(1);
			if (cb) cb(hashtag);
			navigateHashTag(hashtag, newwindow);
		});
	}
	function listenLastlogin($lastLogin, noPrefix){
		var lastLogin = new Date(parseInt($lastLogin.data('lastlogin')) * 1000);
		var now = new Date();
		var active = DateDiff.getDiffFull(lastLogin, now);
		if (active == 0) {
			$lastLogin.html(i18n.t('label_online'));
		} else {
			var text = noPrefix ? i18n.t('label_ago').replace('__date__', active) : i18n.t('label_active_date_ago').replace('__date__', active);
			$lastLogin.html(text);
		}
	}

	function getShareIcon() {
		var isV2_2 = false;
		var ver = window.APPVER || window.VERSION;
		if (ver && ver / 100 >= 202) {
			isV2_2 = true;
		}
		if (isV2_2) {
			var icon_name = 'ic_nva_share_app_no_padding';
		} else {
			var icon_name = 'ic_nva_share_app';
		}
		var shareIconUrl = getIconUrl(icon_name);
		shareIconUrl += "?" + imgVersion;
		console.log(shareIconUrl);
		var shareButtonConfig = {
			type:"button",
			key:"share",
			text:i18n.t('label_share'),
			iconUrl:shareIconUrl
		};
		return shareButtonConfig;
	}

	// assume hashtag text does not contain # character
	function navigateHashTag(hashtag, newwindow, keyword) {
		if (isShopeeWeb() && !keyword) {
			SeoUtil.navigateHashtagSeo(hashtag);
			return;
		}

		var encodedHashtag = encodeURIComponent("#" + (""+hashtag).toLocaleLowerCase());
		var searchParams = {
			is_hashtag: 1,
			hashtag: encodedHashtag
		}

		if (keyword) {
			var url = location.origin + "/search-item/?is_hashtag=1&hashtag=" + encodedHashtag;
			url += '&search=' + encodeURIComponent(keyword);
			var tabs = getSearchNativeTabs(url);

			searchParams[search] = keyword;
		} else {
			var url = location.origin + "/category-item/?is_hashtag=1&hashtag=" + encodedHashtag;
			var tabs = getCategoryNativeTabs(url, true);
		}

		if (window.BI_ANALYTICS){
			var dataV2 = {
				action: 'search',
				data: JSON.stringify(searchParams),
			};

			BI_ANALYTICS.trackingActionEventV2({
				info: dataV2,
			}, BI_ANALYTICS.getV2TrackingEventBaseData(TRACKING_V2_EVENT_TYPE.ACTION));
		}

		if (BJUtil.isRNEnabled('search')) {
			var reactNativeConfig = {module: 'SEARCH_RESULT_PAGE'};
			_.extend(reactNativeConfig, searchParams);
			BJUtil.navigateReactNative(reactNativeConfig);
		} else {
			navigate(url, false, newwindow, {
				url: url,
				preloadKey: 'search',
				/*
				tabRightButton:{
					"type":"button",
					"key":"filter",
					"iconType":"filter",
					"iconText":i18n.t('label_filter')
				},
				*/
				navbar: _getHashtagNavBar(hashtag, keyword)
			});
		}
	}

	function _getHashtagNavBar(hashtag, keyword){
		var navbar = {
		};
		navbar['title'] = '#' + decodeURIComponent(hashtag);
		var rightItems = [
			{'type': 'search'}
		];
		if (isIOS()) {
			navbar['showSearch'] = isVer22Plus() ? 1 : 0;
			if (keyword) {
				navbar['showSearch'] = 1;
			}
		}
		rightItems.push({
			'type': 'button',
			'key': 'filter',
			'iconType': 'filter',
			'iconText': i18n.t('label_filter')
		});
		if (keyword) {
			navbar['searchText'] = decodeURIComponent(keyword);
		}
		if (isVer22Plus()) {
			navbar['searchPlaceholder'] = decodeHTMLEncode('#' + hashtag);
		} else {
			navbar['searchPlaceholder'] = i18n.t('label_search_in') + " " + decodeHTMLEncode('#' + hashtag);
		}
		navbar['searchPlaceholderActive'] = i18n.t('label_search_in') + " " + decodeHTMLEncode('#' + hashtag);
		navbar['searchMatchType'] = 4;
		navbar['searchMatchValue'] = hashtag;
		navbar['searchHotwords'] = {'0':[]};
		navbar['rightItemsConfig'] = {'items': rightItems};
		navbar['searchType'] = 1;
		return navbar;
	}

	function navigateCollection(url, title, keyword) {
		if (keyword) {
			var firstDelim = (url.indexOf('?') >= 0) ? '&' : '?';
			url += firstDelim + 'search=' + encodeURIComponent(keyword);
			var tabs = getSearchNativeTabs(url);
		} else {
			var tabs = getCategoryNativeTabs(url, true, true);
		}

		navigate(url, false, true, {
			url: url,
			navbar: getCollectionNavBar(url, title, keyword)
		});
	}

	function getCollectionNavBar(url, title, keyword){
		var navbar = {
		};
		var matchK = '/collections/';
		var idx = url.indexOf(matchK);
		var collectionid = url.substr(idx + matchK.length);
		if (!parseInt(collectionid)) {
			throw new Error('collection url is invalid!! must follow the format');
		}
		navbar['title'] = title;
		var rightItems = [
			{'type': 'search'}
		];
		if (isIOS()) {
			navbar['showSearch'] = isVer22Plus() ? 1 : 0;
			if (keyword) {
				navbar['showSearch'] = 1;
			}
		}
		rightItems.push({
			'type': 'button',
			'key': 'filter',
			'iconType': 'filter',
			'iconText': i18n.t('label_filter')
		});
		if (keyword) {
			navbar['searchText'] = keyword;
		}
		if (isVer22Plus()) {
			navbar['searchPlaceholder'] = decodeHTMLEncode(title);
		} else {
			navbar['searchPlaceholder'] = i18n.t('label_search_in') + ' ' + decodeHTMLEncode(title);
		}
		navbar['searchPlaceholderActive'] = i18n.t('label_search_in') + " "  + decodeHTMLEncode(title);
		navbar['searchMatchType'] = 3;
		navbar['searchMatchValue'] = collectionid;
		navbar['searchHotwords'] = {'0':[]};
		navbar['rightItemsConfig'] = {'items': rightItems};
		navbar['searchType'] = 1;
		return navbar;
	}

	function listenAnchor($a) {
		$a.off("tap").on("tap", function(e) {
			e.preventDefault();
			e.stopImmediatePropagation();
			e.stopPropagation();
			navigate($a.attr("href"), false, false, {

			})
		})
	}

	function listenShop($shop, newwindow, cb, params, conditionFunc){
		$shop.on('tap', function(e){
			var $self = $(this);
			if (conditionFunc && !conditionFunc($self)){
				return;
			}
			var shopid = $self.data('shopid');
			var username = $self.data('username');
			var shopcat_id = $self.data('shopcat_id');
			if (!shopid) {
				return;
			}
			if (isShopeeWeb() && username) {
				var url = SeoUtil.getShopUrlSeo(username);
				if (shopcat_id){
					url = generateGetUri(url, {shopcat_id: shopcat_id});
				}
				navigate(url);
			} else {
				var url = '/shop/';
				if (!$.isEmptyObject(params)){
					url = url + '?' + $.param(params);
				}
				url += '#shopid=' + shopid + '&shopcat_id=' + shopcat_id;
				navigate(url, false, newwindow, {preloadKey: 'shop', navbar: {isTransparent: 1}});
			}
			if (cb) cb(shopid, $shop);
		});
	}

	function listenUser($user, newwindow, cb){
		$user.off('tap').on('tap', function(e){
			e.stopPropagation();
			e.preventDefault();
			var userid = $(this).data('userid');
			if (!userid){
				return;
			}
			var url = '/shop/user/'+userid;
			if (cb) cb(userid);
			navigate(url, false, newwindow)
		});
	}

	function listenShopItems($shopitems, newwindow, cb){
		$shopitems.on('tap', function(e){
			e.stopPropagation();
			e.preventDefault();
			var shopid = $(this).data('shopid');
			var url = '/shop/#shopid=' + shopid + '&navigate=items';
			if (isShopeeWeb()) {
				url = SeoUtil.getShopUrlSeo($shopitems.data('username') + '#navigate=items');
			}
			if (cb) cb(shopid);
			if (newwindow) {
				navigate(url, false, newwindow, {preloadKey: 'shop', navbar: {isTransparent: 1}});
			} else {
				navigate(url, true);
			}
		});
	}
	function listenRates($rates, newwindow, cb){
		$rates.on('tap', function(e){
			e.stopPropagation();
			e.preventDefault();
			var userid = $(this).data('userid');
			var url = '/buyer/' + userid + '/rating/';
			if (cb) cb(shopid);
			navigate(url, false, newwindow);
		});
	}

	function stringifyFilter(k, v)
	{
		if(k == '__node__' || k == '__image__')
		{
			return undefined;
		}
		return v;
	}

	function itemToString(item)
	{
		return JSON.stringify(item, stringifyFilter);
	}

	function listenItem($item, newwindow, cb, source, itemData, conditionFunc){

		$item.on('tap', function(e){
			e.preventDefault();
			e.stopPropagation();
			$item = $(this);
			if (conditionFunc && !conditionFunc($item)){
				return;
			}
			var shopid = $item.data('shopid');
			var itemid = $item.data('itemid');
			var itemName = $item.find('.itemName').text();
			var jump = $item.data('jump');
			if (isShopeeWeb()) {
				SeoUtil.navigateItemSeo(itemName, shopid, itemid);
			} else {
				var url = '/item/#shopid=' + shopid + '&itemid=' + itemid;
				if (itemData){
					url = url + '&data=' + encodeURIComponent(itemToString(itemData))
				}
				if ($(this).hasClass('image') && jump){
					var sequence = $item.index();
					url += "&image_index=" + sequence;
				}
				navigate(url, false, newwindow, {preloadKey: 'item'});
			}
			url += "&source_url=" + location.href;
			if (cb) cb(itemid, shopid, $item);
		});
	}

	function listenItemSimilar($item, itemData){
		$item.on('tap', function(e){
				e.preventDefault();
				e.stopPropagation();
				$item = $(this);

				var shopid = $item.data('shopid');
				var itemid = $item.data('itemid');
				var itemName = $item.find('.itemName').text();

				if (isShopeeWeb()) {
					var from_flash_sale = true;
					SeoUtil.navigateItemSimilarSeo(itemName, shopid, itemid, from_flash_sale);
				}else{
					var url = '/item/similar/#shopid=' + shopid + '&itemid=' + itemid + '&from_flash_sale=true';
					if (itemData){
						url = url + '&data=' + encodeURIComponent(itemToString(itemData))
					}
					navigate(url,false,null);
				}
			});
	}

	function listenSnapshotItem($item, newwindow, cb, source){
		$item.on('tap', function(e){
			$item = $(this);
			var shopid = $item.data('shopid');
			var itemid = $item.data('snapshot-id');
			var modelid = $item.data('modelid');
			var url = '/item/#shopid=' + shopid + '&itemid=' + itemid + '&modelid=' + modelid +'&snapshot=true';
			var itemName = $item.find('.itemName').text();
			var jump = $item.data('jump');
			if (isShopeeWeb()) {
				SeoUtil.navigateItemSeo(itemName, shopid, itemid, true);
			} else {
				if ($(this).hasClass('image') && jump){
					var sequence = $item.index();
					url += "&image_index=" + sequence;
				}
				navigate(url, false, newwindow, {preloadKey: 'item'});
			}
			if (cb) cb(itemid, shopid);
		});
	}
	function listenProductVideo($videoWrapper){
		$videoWrapper.on('tap', function(e){
			e.preventDefault;
			e.stopPropagation();
			var $this = $(this);
			var bridge = window.WebViewJavascriptBridge;
			if (isVerVideo()) {
				bridge.callHandler("fullScreenImage", {
					imageUrls: [ITEM_IMAGE_BASE_URL + $this.data('image-id')],
					medias: [{
						imageUrl: ITEM_IMAGE_BASE_URL + $this.data('image-id'),
						mediaUrl: ITEM_VIDEO_BASE_URL + $this.data('video-id'),
						type: 1,
						curTime: 0, // This is to tell app to auto play.
					}],
					currentIndex: 0,
					imageX: 0,
					imageY: 0,
					imageWidth: 100,
					imageHeight: 100
				});
			} else {
				bridgeCallHandler('navigate', {
					url: location.origin + "/misc/video/#" + $this.data('video-id'),
					presentModal: 1,
					navbar: isIOS() ? {} : {isTransparent: 1}, // Note: iOS app has a bug that would mess up the navbar if we set it as transparent for modal view in navigate command (but it can still be set inside the page itself).
				});
			}
		});

	}
	function listenComment($comment, newwindow){
		$comment.on('tap', function(e){
			checkLogin(listenCommentAction, {$comment: $comment, e: e, newwindow: newwindow});
		});
	}
	function listenCommentAction(param, newwindow){
		popUserPrivacy();
		var $comment = param.$comment;
		var e = param.e;
		var newwindow = param.newwindow;
		var shopid = $comment.data('shopid');
		var itemid = $comment.data('itemid');
		var feedid = $comment.data('feedid');
		bridgeInit(function(){
			bridge = window.WebViewJavascriptBridge;
			if (feedid) {
				if (bridge) {
					bridge.callHandler("checkVersion", {}, function(e){
						var isV2_2 = false;
						if (isAndroid() && e.appver && e.appver >= 20239) {
							 isV2_2 = true;
						} else if (e.version && e.version >= 20134) {
							isV2_2 = true;
						}
						if (isV2_2) {
							bridge.callHandler("navigateAppPath", {
								path: 'feedComment?feedID=' + feedid + '&shopID=' + shopid
							}, function() {})
						} else {
							navigate('/timeline/comments/' + feedid + '?shopid=' + shopid, false, newwindow);
						}
					});
				} else {
					navigate('/timeline/comments/' + feedid + '?shopid=' + shopid, false, newwindow);
				}
			} else {
				bridgeCallHandler("showItemComments" ,{shopID:shopid,itemID:itemid},function(e){});
			}
		});
	}
	function listenOrder($order) {
		var orderid = $order.data('orderid');
		var userid = USERID;
		var shopid = $order.data('shopid');
		$order.on('tap', function(){
			bridgeInit(function(){
				navigate('/order/detail/?orderid=' + orderid + '&shopid=' + shopid + '&userid=' + userid);
			});
		});
	}
	function listenShare($share){
		$share.on('tap', function(e){
			var $this = $(this);
			var shopid = $this.data('shopid');
			var itemid = $this.data('itemid');
			var avatar = $this.find('.avatar').text();
			var itemName = $this.find('.itemName').text();
			var itemDesc = $this.find('.itemDesc').text();
			var itemCurrency = $this.find('.itemCurrency').text();
			var itemImage = $this.find('.itemImage').text();
			var username = $this.find('.username').text();
			var itemPrice = $this.find('.itemPrice').text();
			var shopName = $this.find('.shopName').text();
			var shopDesc = $this.find('.shopDesc').text();
			var shopImage = $this.find('.shopImage').text();
			var shopUrl = location.origin.replace("mall.", "") + "/" + username;
			var itemUrl = shopUrl + "/" + itemid + "/";
			if (itemid) {
				var params = {
					shopID: shopid,
					avatar: avatar,
					itemID: itemid,
					itemName: itemName,
					itemDesc: itemDesc,
					itemCurrency: itemCurrency,
					itemPrice: itemPrice,
					itemImage: itemImage,
					username: username,
					url: itemUrl,
				};
			} else {
				var params = {
					shopID: shopid,
					shopName: shopName,
					avatar: avatar,
					shopDesc: shopDesc,
					shopImage: shopImage,
					username: username,
					url: shopUrl,
				};
			}
			bridge = window.WebViewJavascriptBridge;
			if (bridge) {
				bridge.callHandler("share",params,function(e){});
			}
		});
	}

	function showWebPopUp(data) {
		var popUp = data.popUp;
		var title = popUp.title || '';
		var message = popUp.message || '';
		var cancelText = popUp.cancelText || '';
		var okText = popUp.okText || '';

		var modal = $('.shopee-confirm-modal');
		var confirmBox = $('.shopee-confirm');

		var buttonClicked = function(e) {
			e.stopPropagation();
			e.stopImmediatePropagation();
			e.preventDefault();
			var buttonClicked = $(this).data('button-id');
			callCallback(data, { buttonClicked: buttonClicked });
			setTimeout(function() {
				modal.addClass('hidden');
				confirmBox.addClass('hidden');
			}, 200);
		};

		confirmBox.find('.confirm-title .content').text(title);
		confirmBox.find('.confirm-message .content').text("");
		var messageParagraphs = message.split("\n");
		_.map(messageParagraphs, function(paragraph){
			confirmBox.find('.confirm-message .content').append($('<p>').html(paragraph));
		});
		confirmBox.find('.flex1').off('tap');

		var $cancel = confirmBox.find('.confirm-button.cancel').text('');
		var $ok = confirmBox.find('.confirm-button.ok').text('');
		var $middle = confirmBox.find('.flex1').text('');

		if (cancelText && okText) {
			$cancel.text(cancelText).off('tap').on('tap', buttonClicked);
			$ok.text(okText).off('tap').on('tap', buttonClicked);
		} else {
			var text = okText || cancelText;
			if (text) {
				$middle.text(text).on('tap', buttonClicked);
			}
		}
		modal.off('tap').on('tap', function(e) {
			e.stopPropagation();
			e.stopImmediatePropagation();
			e.preventDefault();
			setTimeout(function() {
				modal.addClass('hidden');
				confirmBox.addClass('hidden');
			}, 200);
		});

		modal.removeClass('hidden');
		confirmBox.removeClass('hidden');

		if (!title) {
			var $title = confirmBox.find('.confirm-title');
			var titleHeight = $title.height();
			$title.toggle(false);
		}
		$('.shopee-confirm').css({'margin-top':-$('.shopee-confirm').height()/2});
	}

	function showWebPinPopUp(data) {
		var popUp = data.popUp;
		var title = popUp.title;
		var message = popUp.message;
		var cancelText = popUp.cancelText;
		var okText = popUp.okText;
		var numberOfDigits = popUp.numberOfDigits;
		var $popUp = $('#pin-popup');
		var digits = [];
		$popUp.show();
		if (isPC()) {
			$popUp.addClass('pc');
			$popUp.find('.digit-keyboard').hide();
			window.onkeyup = function(e) {
				var key = e.keyCode ? e.keyCode : e.which;
				if (key >= 48 && key <= 58) {
					inputDigit(key-48);
				}
				if (key >= 96 && key <= 105) {
					inputDigit(key - 96);
				}
				if (key == 8) {
					popDigit();
				}
			}
		}
		for (var ele in popUp) {
			$popUp.find('.' + ele).show().text(popUp[ele]);
		}
		$popUp.find('.digit-holder').empty();
		for (var i=0; i<numberOfDigits; i++) {
			$popUp.find('.digit-holder').append($('<div class="digit-input">&nbsp;</div>'));
		}
		var currentIndex = 0;
		$($popUp.find('.digit-input')[currentIndex]).addClass('active');
		function inputDigit(digit) {
			if (currentIndex == numberOfDigits) {
				return;
			}
			$($('.digit-input')[currentIndex]).text('*');
			digits.push(digit);
			currentIndex++;
			$popUp.find('.digit-input').removeClass('active');
			$($popUp.find('.digit-input')[currentIndex]).addClass('active');
		}
		function popDigit() {
			if (currentIndex == 0) {
				return;
			}
			currentIndex--;
			digits.pop();
			$($('.digit-input')[currentIndex]).html('&nbsp;');
			$popUp.find('.digit-input').removeClass('active');
			$($popUp.find('.digit-input')[currentIndex]).addClass('active');
		}
		$popUp.find('.digit').off('tap').on('tap', function(e) {
			e.stopPropagation();
			inputDigit($(this).text());
		});
		$popUp.find('.delete').off('tap').on('tap', function(e) {
			e.stopPropagation();
			popDigit();
		});
		$popUp.find('.okText,.cancelText').on('tap', function(e) {
			var buttonClicked = $(this).data('button-id');
			$popUp.hide();
			callCallback(data, { buttonClicked: buttonClicked, value: digits.join('') });
		});
	}

	function showActionSheet(data, callback) {
		var sheetTitle = data.sheetTitle || '';
		var items = data.items || [];
		var container = $('.shopee-action-sheet-container');
		container.empty();
		var template = new BJDjangofy($('#shopee-action-sheet-item').text());
		for (var i = 0; i < items.length;  i++) {
			var item = items[i];
			container.append(template.render({'data': item}));
		}
		_showActionSheet();
		$('.shopee-action-sheet-mask').off('tap').on('tap', function(e){
			e.preventDefault();
			e.stopPropagation();
			hideActionSheet();
		});
		container.find('.action-sheet-item').on('tap', function(){
			if (callback){
				callback({ btnIndexTapped: $(this).index() });
			} else {
				callCallback(data, { btnIndexTapped: $(this).index() });
			}
			hideActionSheet();
		});
	}

	function _showActionSheet() {
		var container = $('.shopee-action-sheet-container');
		$('.shopee-action-sheet-mask').show();
		/*
			Add padding-bottom to body could prevent the IOS repainting issue for fixed element.
			Reference: http://stackoverflow.com/questions/7826868/fixed-position-navbar-only-clickable-once-in-mobile-safari-on-ios5/10030251#10030251
		*/
		container.delay(20).velocity({
			bottom: '0'
		},{
			duration: 200,
			easing: 'easeInOutQuint',
			begin: function(){$('body').css('padding-bottom', '1px');},
			complete: function(){$('body').css('padding-bottom', '0px');}
		});

		var bridge = window.WebViewJavascriptBridge;
		if (bridge){
			bridge.callHandler("dimNavbar", {
				isDim: true,
				color: "000000",
				alpha: 0.65
			}, function (response) {
			});
		}
	}

	function hideActionSheet() {
		var container = $('.shopee-action-sheet-container');
		$('.shopee-action-sheet-mask').hide();
		container.delay(20).velocity({
			bottom: '-300px'
		},200,'easeInOutQuint');
		var bridge = window.WebViewJavascriptBridge;
		if (bridge) {
			bridge.callHandler("dimNavbar", {
				isDim: false
			}, function (response) {
			});
		}

	}

	function showDropdownMenu(data, callback) {
		var xPosition = data.xPosition || '';
		var yPosition = data.yPosition || '';
		var items = data.items || [];
		var container = $('.shopee-drop-down-menu-container');
		$window = $(window);
		container.empty();
		var template = new BJDjangofy($('#drop-down-menu-item').text());
		for (var i = 0; i < items.length;  i++) {
			var item = items[i];
			container.append(template.render({'data': item}));
		}
		var containerHeight = container.height();
		container.css('opacity', 0);
		container.css('visibility', 'visible');
		// Attention: $(window).height() is the screen height on some ios platform, such as 5c.
		var windowHeight = window.innerHeight ? window.innerHeight : $window.height();
		var yPosInPx = windowHeight * yPosition;
		var menuHeight = containerHeight + 24;
		if (yPosInPx + menuHeight + 20 > windowHeight){
			container.addClass('shopee-drop-down-menu-container--arrow-down');
			container.removeClass('shopee-drop-down-menu-container--arrow-up');
			container.css('top', yPosInPx - menuHeight + 'px');
		} else {
			container.removeClass('shopee-drop-down-menu-container--arrow-down');
			container.addClass('shopee-drop-down-menu-container--arrow-up');
			container.css('top', yPosition * 100 + '%');
		}
		var xPosInPx = $window.width() * xPosition;
		if (isMobile()){
			xPosInPx = xPosInPx + 10;
		}
		container.css('right', $window.width()-xPosInPx-6 + 'px');
		_showDropDownMenu(data);

		container.find('.drop-down-menu-item').on('tap', function(){
			if (callback){
				callback({ btnIndexTapped: $(this).index() });
			} else {
				callCallback(data, { btnIndexTapped: $(this).index() });
			}
			hideDropDownMenu();
		});
	}

	function _showDropDownMenu(data) {
		var container = $('.shopee-drop-down-menu-container');
		var mask = $('.shopee-drop-down-menu-mask').show();
		mask.off('touchmove').on('touchmove', function(e){
			e.stopPropagation();
			e.preventDefault();
			hideDropDownMenu();
			$('.shopee-drop-down-menu-mask').off('touchmove');
		});
		$('body').addClass('noscroll');
		$('html').addClass('noscroll');
		var maskShowDuration = isShopeeApp()? 0: 200;
		mask.delay(20).velocity({
			opacity: 0
		}, {
			duration: maskShowDuration,
		});

		/*
			Add padding-bottom to body could prevent the IOS repainting issue for fixed element.
			Reference: http://stackoverflow.com/questions/7826868/fixed-position-navbar-only-clickable-once-in-mobile-safari-on-ios5/10030251#10030251
		*/
		container.delay(20).velocity({
			opacity: 1
		},{
			duration: 200,
			easing: 'easeInOutQuint',
			begin: function(){$('body').css('padding-bottom', '1px');},
			complete: function(){
				$('body').css('padding-bottom', '0px');
				$('.shopee-drop-down-menu-mask').off('tap').on('tap', function(e){
					e.preventDefault();
					e.stopPropagation();
					hideDropDownMenu();
				});
			}
		});

		//var bridge = window.WebViewJavascriptBridge;
		//if (bridge){
		//	bridge.callHandler("dimNavbar", {
		//		isDim: true,
		//		color: "000000",
		//		alpha: 0.65
		//	}, function (response) {
		//	});
		//}
	}

	function hideDropDownMenu(data) {
		var container = $('.shopee-drop-down-menu-container');
		var mask = $('.shopee-drop-down-menu-mask');
		$('html').removeClass('noscroll');
		$('body').removeClass('noscroll');
		mask.delay(20).velocity({
			opacity: 0
		}, {
			duration: 200,
			complete: function(){ mask.hide()}
		});
		container.delay(20).velocity({
			//height: '0px'
			opacity: 0
		},{
			duration: 200,
			easing: 'easeInOutQuint',
			complete: function(){container.css('visibility', 'hidden')}
		});
		//var bridge = window.WebViewJavascriptBridge;
		//if (bridge) {
		//	bridge.callHandler("dimNavbar", {
		//		isDim: false
		//	}, function (response) {
		//	});
		//}

	}

	function handleSearchHashtag() {
		var bridge = window.WebViewJavascriptBridge;
		bridgeRegisterHandler("search",function(data,x){
			if(data!=null){
				var keyword = data.keyword;
				if (data.type == 1 || data.type == 2) {
					var hashtag = keyword.toLowerCase();
					_track_open_hashtag_list_from_search(hashtag);
					BJUtil.navigateHashTag(hashtag, false);
				}
			}
		});
	}

	function handleFollow(shopid, csrf, success, fail) {
		var bridge = window.WebViewJavascriptBridge;
		$.post("/buyer/follow/shop/"+shopid+"/",{
			"csrfmiddlewaretoken":csrf
		},function(e){
			if (bridge) {
				bridge.callHandler("webNotify",{notifyType:'notifyFollowUserUpdate', shopID:shopid, followed:1});
			}
			if (success) success();
		}).fail(function(e){
			if (fail)
				fail(parseInt(e.responseText));
		});
	}

	function handleUnfollow(shopid, csrf, success, fail) {
		var bridge = window.WebViewJavascriptBridge;
		$.post("/buyer/unfollow/shop/"+shopid+"/",{
			"csrfmiddlewaretoken":csrf
		},function(e){
			if (bridge) {
				bridge.callHandler("webNotify",{notifyType:'notifyFollowUserUpdate', shopID:shopid, followed:0});
			}
			if (success) success();
		}).fail(function(e){
			if (fail)
				fail(parseInt(e.responseText));
		});
	}

	function isV2(callback) {
		if (callback) {
			var bridge = window.WebViewJavascriptBridge;
			// configure navbar search button for v2 and newer
			bridge.callHandler("checkVersion", {}, function(res) {
				var version = res.version;
				if ((isIOS() && version > 20000) || (isAndroid() && version >=28)) {
					callback(true)
				} else {
					callback(false);
				}
			})
		}
	}

	// There is an issue with Safari localStorage to trigger QuotaExceededError in private mode.
	// in this case we just skip over the localStorage IO
	function save(data) {
		var key = data.key;
		var value = data.data;
		var persist = data.persist;
		if (key && value) {
			try {
				window.sessionStorage.setItem(key, value);
				if (persist) {
					window.localStorage.setItem(key, value);
				}
				callCallback(data, { status: 1 });
			} catch (error) {
				console.error(error);
				console.warn('Localstorage is not accessible, maybe the user is using private mode on Safari');
			}
		}
		callCallback(data, { status: 0 });
	}

	function load(data) {
		var result;
		var key = data.key;
		if (key) {
			try {
				result = window.sessionStorage.getItem(key);
				if (!result) {
					result = window.localStorage.getItem(key);
				}
			} catch (error) {
				console.error(error);
				console.warning('Localstorage is not accessible, maybe the user is using private mode on Safari');
			}
		}
		callCallback(data, { data: result });
	}


	function callCallback(requestData, responseData, bjUtilInstance) {
		bjUtilInstance = bjUtilInstance || window.BJBridgeReceiver;
		if (bjUtilInstance) {
			bjUtilInstance.callCallback(requestData, responseData);
		}
	}

	function idxImpl(obj, key, defaultVal, idChecker) {
		var result = defaultVal || null;
		if (obj && idChecker(obj, key)) {
			result = obj[key];
		}
		return result;
	}

	function idx(obj, key, defaultVal) {
		return idxImpl(obj, key, defaultVal, function(object, id) {
			return object && id in object;
		});
	}

	/**
	 * If obj is an object, it will have it's <key> field set to <value>. The original object
	 * will be returned. If obj is null or undefined, a new object is created with <key> field
	 * set to <value>. The new object will be returned.
	 */
	function set(obj, key, value) {
		var result = obj || {};
		result[key] = value;
		return result;
	}

	function replaceAll(s, olds, news)
	{
		if (olds == news){
			return s;
		}
		while(s.indexOf(olds) >= 0)
		{
			s = s.replace(olds, news);
		}
		return s;
	}

	function ownIdx(obj, key, defaultVal) {
		return idxImpl(obj, key, defaultVal, function(object, id) {
			return object && object.hasOwnProperty(id);
		});
	}

	function callFunction(func) {
		if (_.isFunction(func)) {
			return func.apply(func, Array.prototype.slice.call(arguments, 1));
		}
	}

	/**
	 * Return true if code is OK or false otherwise
	 *
	 * @param code [int] beeshop cmd errcode
	 * @param operation [string] the core server operation. Need to match with transifier key
	 */
	function handleProtobufError(code, operation) {
		var msg = '';
		switch (code) {
			case Constants.ProtobufCmdErrcode.LIMIT:
				msg = 'alert_{{operation}}_limit';
				break;
			case Constants.ProtobufCmdErrcode.FREQUENT:
				msg = 'alert_{{operation}}_too_frequent';
				break;
			default:
				if (code !== Constants.ProtobufCmdErrcode.OK) {
					msg = 'msg_server_error';
				}
		}
		if (msg) {
			alert_message(i18n.t(msg.kFormat({operation: operation})));
		}
		return code == Constants.ProtobufCmdErrcode.OK;
	}

	/**
	 * When a control need to respond differently in the global version
	 * use this function to perform the appropriate behavior. It helps
	 * you check what version the locale belongs to.
	 *
	 * @param normal    Normal function to be called by local version.
	 * @param fallback  Fallback function to be called by global version.
	 */
	function globalVersionFallback(normal, fallback) {
		if (BJUtil.isSimpleVersion()) {
			fallback();
		} else {
			normal();
		}
	}
	var PTIME_COUNTDOWNS = [];
	var PTIME_COUNTDOWNER;
	var PtimeCountdown = function ($domElement, ptime, pwait) {
		this.$domElement = $domElement;
		this.ptime = ptime;
		this.pwait = pwait;
		this.endTime = new Date(this.ptime * 1000 + this.pwait * 1000);
		this.updateDom = function() {
			if (DateDiff.inSecs(new Date(), this.endTime) <= 0 ) {
				this.$domElement.html(i18n.t('label_promote_now'));
			}
			this.$domElement.html(DateDiff.inFullFormat(new Date(), this.endTime));
		}
	}
	function listenTicker($tick, limit_check) {
		$tick.off('tap').on('tap', function(e) {
			e.stopPropagation();
			if ($tick.hasClass('ic_checkbox_outline')){
				if ( ! limit_check() ) { return; }
			}
			$tick = $(this);
			$tick.toggleClass('ic_checkbox_outline').toggleClass('ic_checkbox_checked');
		});
	}

	function isInteger(val) {
		return !isNaN(val) && val === parseInt(val, 10);
	}

	var COOKIE_KEY_IS_ADULT = 'SPC_IA';
	// Note: With new batch-item-domify, target_classname will always be undefined/null.
	function checkAdult(is_adult, target_classname) {
		if (!is_adult) {
			return;
		}
		if (!isAdultSafe()) {
			popAdult();
		}
		function setIsAdultSafe() {
			$.cookie(COOKIE_KEY_IS_ADULT, 1, {path:'/'});
		}
		function popAdult() {
			var $target = null;
			if(target_classname)
			{
				$target = $(target_classname);
				// This is in item details page.
				if ($target.width() > 150 ) {
					$target.addClass('adultcntrl-products age-' + ADULT_AGE);
				// This is in item-card.
				} else {
					$target.addClass('adultcntrl age-' + ADULT_AGE);
				}
			}
			bridgeCallHandler('showPopUp', {
				popUp: {
					message:i18n.t('label_age_control_msgs_customize').replace('__{age}__', ADULT_AGE),
					okText:i18n.t('label_age_control_ok_customize').replace('__{age}__', ADULT_AGE),
					cancelText:i18n.t('label_age_control_no_customize').replace('__{age}__', ADULT_AGE),
				}
			}, function (e) {
				// OK
				if (e.buttonClicked == 0) {
					markAdult($target);
				// CANCEL
				} else {
					bridgeCallHandler('popWebView');
				}
			});
		}
	}

	function isAdultSafe() {
		// Note: Somtimes when running from Mac Chrome, $.cookie will be undefined when reaching here. I don't know why.
		if(!$ || !$.cookie)
			return false;
		var c = $.cookie(COOKIE_KEY_IS_ADULT);
		return c == '1';
	}

	function markAdult($target) {
		$.cookie(COOKIE_KEY_IS_ADULT, 1, {expires: 365 * 20, path:'/'});
		$(document).trigger(EVENT_DISMISS_ADULT);

		if(!$target) return;
		$($target).removeClass('adultcntrl');
		$($target).removeClass('adultcntrl-products');
		if ($target.hasClass('itemsimple')) {
			$target.find('.item-card').removeClass('adultcntrl');
		}
	}

	function listenAge($item) {
		var isAdultItem = $item.data('is_adult');
		if (!isAdultItem) {
			return;
		}
		if (!isAdultSafe()) {
			$item.find('.image-wrapper').addClass('adultcntrl age-' + ADULT_AGE);
			if ($item.hasClass('itemsimple')) {
				$item.find('.item-card').addClass('adultcntrl age-' + ADULT_AGE);
			}
		}
	}

	function reInitPage() {
		if (isIOS()) {
			if (window.triggerIOSAppPageReInit) {
				triggerIOSAppPageReInit();
			}
		} else {
			if (isAndroid()) {
				if (window.MeTabView && window.MeTabView.resize) {
					MeTabView.resize(document.body.offsetHeight);
				}
			}
		}
	}

	/**
	 * A helper function to trigger corresponding actions to open a Youtube video link. The actions differ on Web and App
	 * @param $banners {Object} Jquery Dom Object
	 * @param selector {String} CSS Selector for the action trigger button, selected object must have a data-video-id attribute
	 */
	function listenVideo($banners, selector) {
		$banners.find(selector).on('tap', function(e) {
			e.preventDefault();
			e.stopPropagation();
			var videoId = $(e.currentTarget).data('video-id');
			var videoUrl = 'https://youtu.be/' + videoId;
			if (videoId) {
				if (isShopeeWeb()) {
					bridgeCallHandler('navigate', {
						url: videoUrl,
						presentModal: false,
						target: '_blank'
					}, function() {})
				} else {
					var videoCommand = 'openYoutubeVideo';
					bridgeCallHandler('hasHandler', {
						handler: videoCommand
					}, function(res) {
						if (res.status == 1) {
							bridgeCallHandler(videoCommand, {
								videoID: videoId
							}, function(res) {
								if (!res.status) {
									//TODO add proper error handling here
								}
							})
						} else {
							// tricky here !!! this reason use bridge cmd to redirect is that, on iOS, if user installed Youtube,
							// the video url will be parsed like youtube://, etc which is not white list by app
							var bridgeCmdURL = location.origin + '/bridge_cmd/?cmd=inPageNavigate&url=' + encodeURIComponent(videoUrl);
							bridgeCallHandler('openExternalLink', {
								url: bridgeCmdURL
							}, function(res) {
								if (!res.status) {
									//TODO add proper error handling here
								}
							})
						}
					});
				}
			}
		})
	}

	/**
	 * A helper function to set up load more
	 * @param threshold <int> less than this number of pixels, cb will be called
	 * @param cb        <function> callback function
	 */
	function setupLoadMore(threshold, cb) {
		return function() {
			// !!! NOTE: $(window).scrollTop() WILL force synchronous re-layout!!! And jank will happen.
			var scrollTop = scrollTopFunc(); //$(window).scrollTop();
			var viewportHeight = $(window).height();
			var documentHeight = $(document).height();
			if (documentHeight - scrollTop - viewportHeight < threshold) {
				if (cb && typeof(cb) == "function") cb();
			}
		}
	}

	function enterLocationMode(batchItem){
		var $locationBar = $('.location-bar');
		//To make sure the "position: fixed" location bar won't grow too wide on PC.
		if(isPC())
		{
			var width = $('#category-items').width();
			var margin = (($(window).width() - width) / 2) + 'px';
			$locationBar.css({'width':width, 'margin-left': margin, 'margin-right': margin});
		}
		if (!window.lat || !window.lon){
			$locationBar.text(i18n.t('text_waiting_for_location'));
			$locationBar.show();
			if (window.WebViewJavascriptBridge) {
				alert_message_with_loader(i18n.t('text_retrieve_location'));
				window.WebViewJavascriptBridge.callHandler("getLocation",{},function(e){
					hide_message_with_loader();
					if( e.status == 1 )
					{
						//This is clever.
						location = $.query.set('lat',e.latitude).set('lon',e.longitude);
					}
					else{
						showLocationError();
					}
				});
			}
			else {
				$('.location_waitings').removeClass('hidden');
				navigator.geolocation.getCurrentPosition(function(position){
					lat = position.coords.latitude;
					lon = position.coords.longitude;
					//This is clever.
					BJUtil.navigate($.query.set('lat',lat).set('lon',lon) + location.hash, true);
				}, function(){
					showLocationError();
				});
			}
		}
		else{
			onLocationScroll(batchItem);
			$(window).on("scroll", function(){onLocationScroll(batchItem);});
		}
	}
	function hideLocationBar()
	{
		$('.location-bar').hide();
		$('.location-bar').addClass('hidden');
		$(window).off("scroll", onLocationScroll);
	}
	function showLocationError()
	{
		$('.item-list,.loading-text').addClass('hidden');
		$('.nomore').addClass('hidden');
		hide_message_with_loader();
		$('.category_item').hide();
		$('.empty_holder').addClass('hidden');
		$('.empty_holder.location').removeClass('hidden');
		$('.loading-text').hide();
		hideLocationBar();
	}

	var on_location_scroll_last_update_time = 0;
	function onLocationScroll(batchItem){
		//var $li = $(".item-list li");

		var itemsOnScreen = batchItem.itemsOnScreen(false);
		//Wait for i18n to be ready.
		//Wait for item list to be loaded (because we now always dynamically load items, no more server rendering).
		if(!itemsOnScreen || itemsOnScreen.length == 0) //$li.length === 0)
		{
			setTimeout(function(){onLocationScroll(batchItem);}, 200);
			return;
		}

		// !!! NOTE: $(window).scrollTop() WILL force synchronous re-layout!!! And jank will happen.
		var scrolled = scrollTopFunc(); //$(window).scrollTop();
		var now = curTs();
		//MUST do this because when running in-app, our app will callback scroll() for every pixel.
		//The first condition is to properly process the "jump back to top" behavior. If we don't check it, then the location text will not be updated due to the second condtion.
		if(scrolled != 0 && now - on_location_scroll_last_update_time < 500)
			return;
		on_location_scroll_last_update_time = now;

		var $locationBar = $(".location-bar");
		$locationBar.fadeIn(200);
		var precision = itemsOnScreen[itemsOnScreen.length-1].distance;
		//var precision = parseInt($li.eq(0).attr("distance"));
		//var barline = scrolled + 100;
		//$li.each(function(k,v){
		//	var li = $(v);
		//	var top = li.offset().top;
		//	if (top <= barline && (top + li.height() > barline))
		//	{
		//		precision = parseInt(li.attr("distance"));
		//		return false;
		//	}
		//});
		var txt = '';
		if(precision<1000){
			if(precision<50){
				txt = i18n.t('label_within') + ' ' + 50 + i18n.t('label_m');
			}else if(precision<100){
				txt = i18n.t('label_within') + ' ' + 100 + i18n.t('label_m');
			}else if(precision<200){
				txt = i18n.t('label_within') + ' ' + 200 + i18n.t('label_m');
			}else if(precision<500){
				txt = i18n.t('label_within') + ' ' + 500 + i18n.t('label_m');
			}else{
				txt = i18n.t('label_within') + ' ' + 1 + i18n.t('label_km');
			}
		}
		else{
			precision = Math.floor(precision/1000);
			if(precision < 10){
				txt = i18n.t('label_within') + ' ' + precision + i18n.t('label_km');
			}else if(precision<20){
				txt = i18n.t('label_within') + ' ' + 20 + i18n.t('label_km');
			}else if(precision<30){
				txt = i18n.t('label_within') + ' ' + 30 + i18n.t('label_km');
			}else if(precision<50){
				txt = i18n.t('label_within') + ' ' + 50 + i18n.t('label_km');
			}else if(precision<100){
				txt = i18n.t('label_within') + ' ' + 100 + i18n.t('label_km');
			}else{
				txt = i18n.t('label_more_than_200km_away');
			}
		}
		$locationBar.text(txt);
	}

	/**
	 * Go to custom select page, select an option and then direct to targetUrl,
	 * the selected index will pass to server via query string, using key 'custom_select_index'
	 * @param options		an array of option text
	 * @param title			title of select page
	 * @param defaultIndex	default selected index
	 * @param navigateParam		parameter for navigate command
	 */
	function customSelectThenNavigate(options, title, defaultIndex, navigateParam, infoText) {
		if (infoText)
			localStorage.setItem("custom_select_info_text", infoText);

		var hashedString = "";
		options.forEach(function(text, index) {
			var keyValuePair = "option"+index+"="+ encodeURIComponent(text);
			hashedString = index == 0 ? hashedString : hashedString + "&";
			hashedString += keyValuePair;
		});
		if (!isNaN(defaultIndex) && defaultIndex >= 0) {
			hashedString += "&selected=" + defaultIndex;
		}
		//hashedString += "&target_url=" + targetUrl;
		localStorage.setItem("custom_select_text", hashedString);
		var selectUrl = '/func/app_select/#navigate=' + encodeURIComponent(JSON.stringify(navigateParam));
		//alert('navigate to: ' + selectUrl);
		if (isShopeeApp()) {
			bridgeCallHandler("navigate",{
				url: location.origin + selectUrl,
				navbar: {
					title : title
				},
				config: {disableReload: 1}
			}, function(e){});
			//bridgeCallHandler("save", {
			//	key : "select-name",
			//	data : ,
			//	persist : 0
			//}, function(status) {
			//	if (status) {
			//		//navigate to custom-select
			//
			//	}
			//});
		}else{
			//sessionStorage["select-name"] = $(select).attr("id");
			BJUtil.navigateModally(location.origin + selectUrl, "custom_select");
		}
	}


	var listenCategoryLink = function(t, cb){
		$(t).on('tap', function(e){
			var $this = $(this);
			e.preventDefault();
			e.stopPropagation();
			var catid = $this.data('catid');
			var catname = $this.data('catname');
			var noSub = $this.data('nosub');
			if (cb) cb($this);
			handleNavigateCategory(catid, catname, noSub, window.bridge);
		});
	};

	var handleNavigateSearch = function(keyword, catid, catname, by, appBridge){
		var url = location.origin + "/search-item/?search=" + encodeURIComponent(keyword);
		if (catid && catid != ''){
			url = url + '&catid=' + encodeURIComponent(catid);
		}
		if (catname && catname != ''){
			url = url + '&catname=' + encodeURIComponent(catname);
		}
		if (by && by != ''){
			url = url + '&by=' + encodeURIComponent(by);
		}
		var navBar = getSubCateNavBar(catid, catname);
		navBar['searchText'] = keyword;
		bridgeCallHandler("navigate", {
			url: url,
			preloadKey: 'search',
			tab: getCategoryNativeTabs(url),
				tabRightButton:{
				"type":"button",
				"key":"filter",
				"iconType":"filter",
				"iconText":i18n.t('label_filter')
			},
			navbar:navBar
		}, function(){});
	};

	function obtainLocation(callback)
	{
		setLocationBarText(i18n.t('text_waiting_for_location'));

		var scrollTop = scrollTopFunc();
		if (window.WebViewJavascriptBridge)
		{
			alert_message_with_loader(i18n.t('text_retrieve_location'));
			window.WebViewJavascriptBridge.callHandler("getLocation",{},function(e){
				hide_message_with_loader();
				if( e.status == 1 )
				{
					callback(e.latitude, e.longitude);
				}
				else
				{
					showLocationError();
				}
			});
		}
		else {
			navigator.geolocation.getCurrentPosition(function(position){
				callback(position.coords.latitude, position.coords.longitude);
			}, function(){
				showLocationError();
			});
		}
		function showLocationError(){
			setLocationBarText(i18n.t('text_retrieve_location_failed'));
		}
	}

	function setLocationBarText(t)
	{
		$('.location-bar').text(t);
	}

	// called when click "sub category", "3LC" or any other page that will navigate to search result page
	var listenSearch = function(t, cb){
		$(t).on('tap', function(e){
			var $this = $(this);
			e.stopPropagation();
			var keyword = $this.data('keyword');
			var catid = $this.data('catid');
			var catname = $this.data('catname');
			var by = $this.data('by');
			if (cb) cb($this);
			handleNavigateSearch(keyword, catid, catname, by, window.bridge)
		});
	};

	function jumpToSearch(params) {
		var mainCategoryId = params.mainCategoryId || null;
		var subCategoryId = params.subCategoryId || null;
		var isAdult = params.isAdult || null;
		var keyword = params.keyword || null;
		var mainCategoryName = params.mainCategoryName || '';
		var inPageReload = !!params.inPageReload;
		var nextPageInPageReload = !!params.nextPageInPageReload;
		var pageType = params.pageType || 'search';
		var shopid = params.shopid || null;
		var shopCategoryIds = params.shopCategoryIds || null;
		var searchParams = {};
		var shopCategoryName = params.shopCategoryName || null;
		var shopName = params.shopName || null;
		var searchThisThenGoBack = params.searchThisThenGoBack || null;
		var matchid = params.matchid || null;
		var subCategoryName = params.subCategoryName || null;
		var filterObj = params.filterObj || null;
		var hashtag = params.hashtag || null;
		var source = params.source || null;
		var suggestedWords = params.suggestedWords || [];
		var originalCategoryId = params.originalCategoryId || null;
		var isOfficialShop = params.isOfficialShop || null;
		if (keyword) {
			searchParams['search'] = encodeURIComponent(keyword);
		}
		if (subCategoryId) {
			searchParams['sub_categoryid'] = subCategoryId;
		}
		if (isAdult) {
			searchParams['is_adult'] = isAdult;
		}
		if (mainCategoryName) {
			searchParams['categoryname'] = mainCategoryName;
		}
		if (mainCategoryId && mainCategoryId != -1) {
			searchParams['categoryid'] = mainCategoryId;
		}
		if (subCategoryName) {
			searchParams['sub_cate_name'] = subCategoryName;
		}
		if (pageType == 'shop') {
			searchParams['is_shop'] = 1;
		} else if (pageType == 'hashtag') {
			searchParams['is_hashtag'] = 1;
		} else if (pageType == 'attribute'){
			searchParams['is_attribute'] = 1;
		}
		if (shopid) {
			searchParams['shopid'] = shopid;
		}
		if (shopCategoryIds) {
			searchParams['shop_categoryids'] = shopCategoryIds;
		}
		if (shopName) {
			searchParams['shop_name'] = shopName;
		}
		if (shopCategoryName) {
			searchParams['shop_cate_name'] = shopCategoryName;
		}
		if (nextPageInPageReload) {
			searchParams['in_page_reload'] = true;
		}
		if (searchThisThenGoBack) {
			searchParams['search_back'] = searchThisThenGoBack;
		}
		if (matchid) {
			searchParams['match_id'] = matchid;
		}
		if (filterObj) {
			searchParams['filter_obj'] = filterObj;
		}
		if (hashtag){
			searchParams['hashtag'] = hashtag;
		}
		if (source){
			searchParams['source'] = source;
		}
		if (suggestedWords) {
			searchParams['suggested_words'] = JSON.stringify(suggestedWords);
		}
		if (originalCategoryId) {
			searchParams['original_categoryid'] = originalCategoryId;
		}
		if (isOfficialShop) {
			searchParams['is_official_shop'] = true;
		}
		var url = null;
		var preloadKey = null;

		if (pageType == 'collection') {
			url = location.origin + '/collections/' + matchid + '/?' + $.param(searchParams);
			preloadKey = null;
		} else {
			url = location.origin + '/search-item/?' + $.param(searchParams);
			preloadKey = 'search';
		}

		if (window.BI_ANALYTICS){
			var dataV2 = {
				action: 'search',
				data: JSON.stringify(searchParams),
			};

			BI_ANALYTICS.trackingActionEventV2({
				info: dataV2,
			}, BI_ANALYTICS.getV2TrackingEventBaseData(TRACKING_V2_EVENT_TYPE.ACTION));
		}

		navigate(url, inPageReload, false, { preloadKey: preloadKey })
	}

	function showMsgAndGoBack(msg)
	{
		bridgeCallHandler('showPopUp', {
			popUp: {
				message: msg,
				okText: i18n.t("label_ok")
			}
		}, function(){
			bridgeCallHandler('popWebView', {}, null);
		});
	}

	function showMsg(msg)
	{
		bridgeCallHandler('showPopUp', {
			popUp: {
				message: msg,
				okText: i18n.t("label_ok")
			}
		});
	}

	function goBackOrClose(ignoreModal)
	{
		function getPathFromReferrer() {
			var a = document.createElement('a');
			a.href = document.referrer;
			return a.pathname;
		}

		// header config
		function isFromExternal() {
			var whiteList = [
				location.host,
				location.host.replace('mall.', 'seller.'),
				location.host.replace('mall.', ''), // for PC mall domain
				location.host.replace('mall.', 'lite.'), // for Shopee Lite domain
				location.host.replace('mall.', 'chatbot.'),
				location.host.replace(/payment./, ''), // for svs cn domain, back button should back to svs
				location.host.replace(/payment.(sg|my|tw|ph|vn|th|id|br)./, ''), // for svs cn domain, back button should back to svs
			];
			return whiteList.reduce(function(result, url) {
				return result && document.referrer.indexOf(url) == -1;
			}, true) || window.history.length === 1;
		}

		function shouldGoToHome() {
			var path = getPathFromReferrer();
			var include = ['/buyer/payment/cybs/commit/'];
			var regexInclude = [/\/buyer\/payment\/\d+\/ipay\//g];
			var regexCurrentInclude = [/\/me\//g];
			return isFromExternal() ||
				include.indexOf(path) >= 0 ||
				regexInclude.some(function(regex) {
					return regex.test(path);
				}) ||
				regexCurrentInclude.some(function(regex) {
					return regex.test(location.href);
				});
		}

		function shouldGoToMe() {
			var path = getPathFromReferrer();
			var include = ['/buyer/payment/ipay/commit/'];
			return include.indexOf(path) >= 0;
		}

		if(history.length == 1 && window.opener != null && window.opener != window)
		{
			window.close();
		}
		else if(shouldGoToHome())
		{
			BJUtil.navigate('/');
		}
		else if (shouldGoToMe())
		{
			BJUtil.navigate('/me/');
		}
		// !!! Modal web view is implemented using iFrame, and can only be dismissed by calling popWebView of bridgeReceiver.
		// It's very important to check this isInModal() condition before calling popWebView because it will infinite loop if we are not in a modal view (as bridgeReceiver's popWebView would call goBackOrClose() if it's not a modal view).
		else if (parent.BJUtil.isInModal() && !ignoreModal)
		{
			bridgeCallHandler('popWebView', {}, null);
		}
		else
		{
			window.history.back();
			// If no more back to go, close self.
			setTimeout(function(){
				if(window.opener != null && window.opener != window)
					window.close();
			}, 300);
		}
	}

	var itemDisplayCardDjangofyCache = {};
	var renderItemDisplayCardTemplates = null;
	// It is the smaller item card, which is used for horizontal scroll area, such is shop category, similar products, etc.
	function renderItemDisplayCard(itemData, isSeeMoreItem, isDummyItems, isSimple, clickFunc){
		function _loadTemplate() {
			renderItemDisplayCardTemplates = {};
			renderItemDisplayCardTemplates.templateItem = isSimple? $('#simple-item-card').text():$('#item-card-display').text();
			renderItemDisplayCardTemplates.templateItemSeeMore = isSimple? $('#simple-item-card').text():$('#item-card-display-see-more').text();
			renderItemDisplayCardTemplates.templateBadgePromotion = $("#badge-promotion").text();
			renderItemDisplayCardTemplates.templateBadgeNew = $("#badge-new").text();
			renderItemDisplayCardTemplates.templateBadgeSoldout = $("#badge-soldout").text();
		}
		function _renderItem(data, isSeeMoreItem) {
			var djangofyCacheType = isSeeMoreItem? 'seeMore': 'normal';
			if(!itemDisplayCardDjangofyCache[djangofyCacheType])
			{
				var djangofyTemplate = isSeeMoreItem? renderItemDisplayCardTemplates.templateItemSeeMore: renderItemDisplayCardTemplates.templateItem;
				itemDisplayCardDjangofyCache[djangofyCacheType] = new BJDjangofy(
					djangofyTemplate, {
						'template_subs': {
							'shop/badges/badge_new.html' : renderItemDisplayCardTemplates.templateBadgeNew,
							'shop/badges/badge_promotion.html' : revertTWDiscountForEnglishIfNeededTemplate(renderItemDisplayCardTemplates.templateBadgePromotion),
							'shop/badges/badge_soldout.html' : renderItemDisplayCardTemplates.templateBadgeSoldout
						}
					}
				);
			}
			revertTWDiscountForEnglishIfNeeded(data);
			return $(itemDisplayCardDjangofyCache[djangofyCacheType].render({
				'ITEM_IMAGE_BASE_URL':ITEM_IMAGE_BASE_URL,
				'item': data,
				'LOCALE': window.LOCALE,
				'LANGUAGE': window.LANGUAGE,
			}));
		}

		function _processRatingInfo(item){
			var roundedScore = Number(item.rating_star).toFixed(1);
			for (var i=0;i<5; i++){
				var current_score = roundedScore - i;
				if (current_score >= 1){
					item['star_cls_'+i] = 'ic_rating_score_solid'
				} else if (current_score >= 0.5){
					item['star_cls_'+i] = 'ic_rating_score_half'
				} else {
					item['star_cls_'+i] = 'ic_rating_score_hollow'
				}
			}
			if (item.rating_count && item.rating_count.length > 0 && item.rating_count[0]){
				item.rating_count_display = '(' + item.rating_count[0] + ')';
				item.rating_inline_style = ''
			}  else {
				item.rating_inline_style = 'visibility:hidden'
			}
			return item
		}



		if (!renderItemDisplayCardTemplates){
			_loadTemplate();
		}

		var windowWidth = $('.center-container').width();
		var cardWidth = Math.round(windowWidth * 0.28);

		if (!isSimple){
			itemData = _processRatingInfo(itemData);
		}
		var $item = _renderItem(itemData, isSeeMoreItem);
		$item.css('width', cardWidth);
		if (!isDummyItems) {
			if (!isSeeMoreItem){
				listenItem($item, null, clickFunc, null, itemData);
				listenAge($item);
				if (!isSimple) {
					$item.find('.item_rating').off('tap').on('tap', function (e) {
						e.preventDefault();
						e.stopPropagation();
						var targetUrl = '/shop/{0}/item/{1}/rating/'.f($item.data('shopid'), $item.data('itemid'));
						BJUtil.navigate(targetUrl);
					});
				}
			}
			// !!! DOM-trashing warning. DO NOT DO IT HERE. Do it in outer-most part.
			// dollarfy_element($item);
		}
		return $item;
	}

	function showSellerBlockedUserPopup(){
		bridgeCallHandler('showPopUp', {
			popUp: {
				message: i18n.t('msg_seller_has_blocked_you'),
				cancelText: i18n.t('label_cancel'),
				okText: i18n.t('label_see_other_products')
			}
		}, function(e){
			if (e.buttonClicked == 0){
				if (isMobile()) {
					var pathJson = {};
					var ver = window.APPVER || window.VERSION;
					if ((isIOS()&&ver>=20809)||(isAndroid()&&ver>=20803)){
						// We remove currentPageUrl support and improve lastPageTrigger since this version.
						pathJson["lastPageTrigger"] = JSON.stringify({'action': 'addHash', 'hash': 'just-for-you'});
					} else {
						pathJson["currentPageUrl"] = location.origin + "/#just-for-you";
					}
					var pathStr = JSON.stringify(pathJson);
					var navRoute = encodeURIComponent(Base64.encode(pathStr));
					var url = 'home?navRoute=' + navRoute;
					bridgeCallHandler('jump', {path: url}, function () {
					});
				} else {
					location.href = "/#just-for-you";
				}
			}
		});
	}

	// function with side effects
	function redirectToPC(url) {
		BJUtil.navigate('//' + location.hostname.replace('mall.', '') + url, true);
	}

	function getPriceForDisplay(rawPrice) {
		var COUNTRY_PRICE_ROUNDING_CONFIG = {
			'TW': 100,
			'ID': 10000,
			'TH': 100,
			'MY': 10,
			'VN': 10000,
			'PH': 50,
			'SG': 10,
		};
		var price = rawPrice / PRICE_INFLATION_FACTOR;
		var rounding = COUNTRY_PRICE_ROUNDING_CONFIG[LOCALE];
		price = Math.round(price/rounding) * rounding;
		return price;
	}

	function encryptPassword(password) {
		// crypto.js must be included
		var password = CryptoJS.SHA256(CryptoJS.MD5(password).toString()).toString();
		return password;
	}

	function _dimNavBar(){
		bridgeCallHandler("dimNavbar", {
				isDim: true,
				color: "000000",
				alpha: 0.54
			}, function (response) {}
		);
	}

	function toggleGeneralMask(isDisplay, maskZIndex, handleTapBack){
		var bridge = window.WebViewJavascriptBridge;
		var $generalMask = $('.general-mask');
		if (isDisplay){
			if (maskZIndex){
				$generalMask.css('z-index', maskZIndex) // Because .header is 1001.
			}
			$generalMask.show();
			if (isShopeeApp()){
				if (isIOS()){
					if (!document.hidden){
						// This is to check whether user has switch tab
						_dimNavBar();
					}
				} else {
					_dimNavBar();
				}
				bridgeRegisterHandler("viewWillReappear",function(e,x){
					if ($('.general-mask').is(':visible')){
						_dimNavBar();
					}
				});
			}
			$generalMask.off().on("tap", function(e){
				e.preventDefault();
				e.stopImmediatePropagation();
				toggleGeneralMask(false, maskZIndex, handleTapBack)
			}).on('touchmove', function (e) {
				e.stopPropagation();
				e.preventDefault();
			});

			if (handleTapBack){
				bridgeRegisterHandler('didTapBack', function() {
					toggleGeneralMask(false, maskZIndex, handleTapBack)
				}, _getTapBackHandlerId());
			}

		} else {
			_hideMask();
			if (handleTapBack && bridge){
				bridge.unregisterHandler('didTapBack', _getTapBackHandlerId());
			}
		}

		function _hideMask(){
			$generalMask.hide().css('z-index', '');
			bridgeCallHandler("dimNavbar", {
				isDim: false,
			}, function (response) {});
		}

		function _getTapBackHandlerId(){
			return "toggleGeneralMaskTapBackHandler"
		}
	}


	function updateGeoInfo(address_id, geoinfo, always_callback) {
		$.post('/addresses/geo_info/', {"csrfmiddlewaretoken": csrf, address_id: address_id, geoinfo: geoinfo})
			.always(function() {
				if (always_callback) {
					always_callback();
				}
			});
	}

	function disableLogisticsChannel(channels, success, fail) {
		var postData = {};
		for (var i=0; i<channels.length; i++) {
			postData[channels[i]] = {enabled: false};
		}
		$.post('/seller/logistics/shipping/shop/update/', {
			settings: JSON.stringify(postData),
			csrfmiddlewaretoken: csrf,
		}, success).fail(fail);
	}

	function disableCodLogisticsChannel(channels, success, fail) {
		var postData = {};
		for (var i=0; i<channels.length; i++) {
			postData[channels[i]] = {
				cod_enabled: false,
				enabled: SUPPORT_ADDRESS_CHECK_LOGISTICS_DICT[channels[i]].enabled,
				seller_preferred: SUPPORT_ADDRESS_CHECK_LOGISTICS_DICT[channels[i]].seller_preferred,
			}
		}
		$.post('/seller/logistics/shipping/shop/update/', {
			settings: JSON.stringify(postData),
			csrfmiddlewaretoken: csrf,
		}, success).fail(fail);
	}

	function disableLogisticsChannelAndCodChannel(channels, cod_channels, success, fail) {
		var postData = {};

		for (var i=0; i<cod_channels.length; i++) {
			postData[cod_channels[i]] = {
				cod_enabled: false,
				enabled: SUPPORT_ADDRESS_CHECK_LOGISTICS_DICT[cod_channels[i]].enabled,
				seller_preferred: SUPPORT_ADDRESS_CHECK_LOGISTICS_DICT[cod_channels[i]].seller_preferred,
			}
		}

		// override post data if channels are also disabled
		for (var i=0; i<channels.length; i++) {
			postData[channels[i]] = {enabled: false};
		}

		$.post('/seller/logistics/shipping/shop/update/', {
			settings: JSON.stringify(postData),
			csrfmiddlewaretoken: csrf,
		}, success).fail(fail);
	}

	function updateChannelStatus(address, success, fail) {
		$.post('/seller/logistics/shipping/shop/update/', {
			address: JSON.stringify(address),
			csrfmiddlewaretoken: csrf,
		}, success).fail(fail);
	}

	function navigateToMap(joinedAddress, lat, lon, isViewOnly, divisionAddress) {
		divisionAddress = typeof divisionAddress !== 'undefined' ? divisionAddress : '';

		var restriction = '&match=' + encodeURIComponent(JSON.stringify(['admin3', 'admin2', 'admin1', 'country']));
		var mapData = '&address=' + joinedAddress + '&division_address=' + divisionAddress;
		var viewOnly = isViewOnly ? '&viewOnly=true' : '';
		if (lat && lon) {
			mapData += '&latitude=' + lat + '&longitude=' + lon;
		}
		if (!isShopeeApp()) {
			showPopup(i18n.t('msg_app_geolocation_address'), i18n.t('label_ok'));
			return;
		}
		bridgeCallHandler("checkVersion", {}, function(e){
			var isSupported = false;
			if (isAndroid() && e.appver && e.appver >= 21000) {
				 isSupported = true;
			} else if (e.version && e.version >= 21000) {
				isSupported = true;
			}
			if (isSupported) {
				if (isAndroid()) {
					bridgeCallHandler('isMapAvailable', {}, function(e) {
						if (e.status == 1) {
							bridgeCallHandler("navigateAppPath", {
								path: 'reactNative?bundle=shopee&module=MAP_PAGE' + restriction + mapData + viewOnly,
							}, function() {})
						} else {
							showPopup(i18n.t('msg_download_shopee_google'), i18n.t('label_ok'));
						}
					});
				} else {
					bridgeCallHandler("navigateAppPath", {
						path: 'reactNative?bundle=shopee&module=MAP_PAGE' + restriction + mapData + viewOnly,
					}, function() {})
				}
			} else {
				showPopup(i18n.t('msg_pls_update_apps'), i18n.t('label_ok'));
			}
		});
	}

	function calculateLocationInAds(idx) {
		if (idx < 2) {
			return idx;
		}
		return Math.floor((idx - 2) / 5 + 1);
	}

	function getTrackingReferUrls(itemid) {
		return ShopeeLib.Adc.getReferals(itemid);
	}

	function showPopup(message, okText, cancelText, callback, title){
		var params = {
			buttonType:1,
		};
		params.message = message || '';
		params.title = title || '';
		params.okText = okText || i18n.t('label_ok');
		params.cancelText = cancelText || i18n.t('label_cancel');
		bridgeCallHandler('showPopUp',{
			popUp: params
		},function(e){
			callback && callback(e);
		});
	}

	function showConfirmPopup(message, callback, title){
		var params = {
			message: message || '',
			okText: i18n.t('label_ok'),
			title: title || ''
		};
		bridgeCallHandler('showPopUp',{
			popUp: params
		},function(e){
			callback && callback(e);
		});
	}

	function showPopupWithLearnMore(message, questionId, okText, cancelText, callback, learnMoreUrl, title){
		var popUpConfig = {
			message: message,
			okText: okText || i18n.t('label_ok')
		}
		if (questionId || learnMoreUrl){
			popUpConfig.cancelText = cancelText || i18n.t('label_learn_more');
		}
		if (title){
			popUpConfig.title = title;
		}
		bridgeCallHandler('showPopUp', {
			popUp: popUpConfig
		}, function(e) {
			if (e.buttonClicked == 1 && (questionId || learnMoreUrl)) {
				var url = questionId? getFAQAnswerUrl(questionId): learnMoreUrl;
				BJUtil.navigate(url, false, true);
			}
			callback && callback(e);
		});
	}

	function is_ads_topup_shopid(shopid){
		// To use this, please render ADS_TOPUP_SHOPIDS in to page.
		if (window.ADS_TOPUP_SHOPIDS && window.ADS_TOPUP_SHOPIDS.indexOf(parseInt(shopid)) !=- 1){
			return true;
		}
		return false;
	}

	function disablePageScroll(pageScrollTop, pageScrollLeft){
		// Disable scroll by setting the page as fixed element;
		// Currently used by shopping cart model panel
		// Because the variation part need scroll,
		// we need this to prevent it scroll the page when at the edge of variation part.
		if (!pageScrollTop){
			pageScrollTop = $(window).scrollTop();
		}
		if (!pageScrollLeft){
			pageScrollLeft = 0;
		}
		var $document = $(document);
		window._pageScrollTop = pageScrollTop;
		$('body')
			.css('top', -window._pageScrollTop+'px')
			.css('height', $document.height())
			.css('width', $document.width())
			.css('left', pageScrollLeft)
			.addClass('disable-scroll');
	}

	function enablePageScroll(){
		$('body').removeClass('disable-scroll');
		$(window).scrollTop(window._pageScrollTop);
	}

	function showVoiceOTPPopUp(onConfirm) {
		bridgeCallHandler('showPopUp', {
			popUp: {
				message: i18n.t('msg_you_will_receive_a_phone_call'),
				okText: i18n.t('label_call_me'),
				cancelText: i18n.t('label_cancel')
			}
		}, function (e) {
			if (e.buttonClicked == 0){
				onConfirm();
			}
		});
	}

	function getIconUrl(filename) {
		var base, iconUrl;
		if (window.MALL_HOST_ROOT_URL){
			base = window.MALL_HOST_ROOT_URL + '/static/images/';
		} else {
			base = location.protocol + "//" + location.host + '/static/images/';
		}
		if (window.devicePixelRatio >= 4) {
			var imgUrl = base + filename + '_4x.png'; // cannot use "@3x or @2x" as android won't support it in the url
			var iconImg = new Image();
			iconImg.src = imgUrl;
			if (iconImg.height > 0) {
				iconUrl = imgUrl;
			}
		} else if (window.devicePixelRatio >= 3) {
			iconUrl = base + filename + '_3x.png';
		} else if (window.devicePixelRatio >= 2) {
			iconUrl = base + filename + '_2x.png';
		} else {
			iconUrl = base + filename + '.png';
		}
		return iconUrl + "?" + imgVersion
	}

	function getIconNameWithRatio(iconName) {
		// Note: it should only be used with icons having the 5 ratios and with .png extension.
		var fileName = iconName.split('.png')[0];
		var ratioPart = '';
		if (window.devicePixelRatio >= 1.5 && window.devicePixelRatio < 2){
			ratioPart = '@1.5x';
		} else if (window.devicePixelRatio >= 2 && window.devicePixelRatio < 3) {
			ratioPart = '@2x';
		} else if (window.devicePixelRatio >= 3 && window.devicePixelRatio < 4) {
			ratioPart = '@3x';
		} else if (window.devicePixelRatio >= 4) {
			ratioPart = '@4x';
		}
		return fileName + ratioPart + '.png'
	}

	function navigateReactNative(pathParams, popSelf){
		var defaultPathParams = {
			bundle: 'shopee',
			refer_url: location.href.split(/#|\?/)[0]
		};
		var appPath = 'reactNative';
		bridgeCallHandler("navigateAppPath", {
			'path': generateGetUri(appPath, _.extend(defaultPathParams, pathParams)),
			'popSelf': !!popSelf, // popself should only be boolean field, otherwise it will break in Android since JAVA is strong type.
		})
	}

	function isRNEnabled(page) {
		if($.cookie('FORCE_DISABLE_RN') === '1') {
			// This cookie is written by App to force disable RN.
			return false;
		}
		var appver = getAppVersion();
		var RNVer = getRNBundleVersion();
		var AppVersionRequirementsForRNPage = isAndroid() ? window.RN_PAGE_MIN_VERSION_ANDROID : window.RN_PAGE_MIN_VERSION_IOS;
		var BundleVersionRequirementsForRNPage = window.RN_PAGE_MIN_VERSION_BUNDLE;

		if (typeof(AppVersionRequirementsForRNPage)==="undefined" || typeof(BundleVersionRequirementsForRNPage)==="undefined"){
			// if both are undefined, means it's not server rendered page, is 3rd party page, should redirect to RN
			return true;
		}

		var isPassAppVersionCheck = appver >= (AppVersionRequirementsForRNPage[page] || AppVersionRequirementsForRNPage['default']);
		var isPassBundleVersionCheck = RNVer >= (BundleVersionRequirementsForRNPage[page] || BundleVersionRequirementsForRNPage['default']);
		var RNPageOpenPercentage = window.RN_PERCENTAGE[page];
		if (typeof RNPageOpenPercentage === 'undefined') {
			RNPageOpenPercentage = window.RN_PERCENTAGE['default'];
		}

		if (!isIOS7() && isPassAppVersionCheck && isPassBundleVersionCheck) {
			// precedence: mall disable > app enable > traffic control
			if (RNPageOpenPercentage == -1){
				// means mall disable the entrance for this feature.
				return false;
			}

			var appEnableRN = $.cookie('ENABLE_RN') === '1'; // This cookie is written by App to force enable RN.
			if (appEnableRN) {
				return true;
			} else if (!!USERID) {
				return USERID % 100 < RNPageOpenPercentage;
			} else {
				return RNPageOpenPercentage == 100;
			}
		} else {
			return false;
		}
	}

	function addFromTabFilterToUrl(url, fromFilterTab){
		if (fromFilterTab){
			return generateGetUri(url, {'filter': fromFilterTab});
		} else {
			return url;
		}
	}

	// This is an enhanced getJSON function with cache
	function getJSONPlus(url, cb, fail) {
		var cacheKey = url; // Might need to add more variables depends on the usage
		var promise = $.Deferred();
		bridgeCallHandler('load', {key: cacheKey}, function(data) {
			if (data["data"] != null) {
				// if has local data, serve first
				var savedData = JSON.parse(data.data);
				cb(savedData);
				// then fetch latest data
				fetchFromServer(url, savedData, cacheKey);
				promise.resolve();
			} else {
				// if no local data, directly fetch from server
				fetchFromServer(url, null, cacheKey);
			}
		});
		return promise;
		function fetchFromServer(url, savedData, cacheKey) {
			getJSON(url, function(serverData) {
				if (!savedData || serverData.version != savedData.version) {
					// if no local data, or local data is outdated
					cb(serverData);
					bridgeCallHandler('save', {
						key: cacheKey,
						data: JSON.stringify(serverData),
						persist: 0
					});
				}
				promise.resolve();
			}, null, 10000).fail(function(e) {
				if (fail) {
					fail(e);
				}
				promise.resolve();
			});
		}
	}

	function _jumpToOfficialShopLanding(catid, tabs, initPosition) {
		var additionalConfig = {};
		tabs = tabs || [];
		var tabIndex = _.map(tabs, function (tab) {
			return tab.catid;
		}).indexOf(catid);
		if (tabIndex == -1) {
			tabIndex = Constants.OFFICAL_SHOP_POPULAR_CATID;
		}

		additionalConfig.navbar = getOfficialShopNavBar();
		if (tabs.length > 1){
			additionalConfig.tabs = tabs;
			additionalConfig.tabsConfig = {
				textColor: '#212121', //disabled. use default color.
				activeColor: '#D0011B',
				barColor: '#FFFFFF',
				textSize: 14,
				textSizeActive: 16,
				tabIndex: tabIndex || 0,
			};
			additionalConfig.pageType = SEARCH_ENUM.PAGE_TYPE_ENUM.OFFICAL_SHOP_LANDING;
		}

		var params = {
			catid: catid || Constants.OFFICAL_SHOP_POPULAR_CATID,
		};
		if (tabs && tabs.length){
			params.tabs_count = tabs.length;

			if (initPosition != null) {
				params.init_position = initPosition;
				var _url =  tabs[0]['url'];
				var hashPart = parse_hash_general(_url) || {};
				var path = _url.split("#")[0];
				hashPart['init_position'] = initPosition;
				tabs[0]['url'] = generateHashUri(path, hashPart);
			}

			BJUtil.navigate('', null, null, additionalConfig);

		} else {
			var targetUrl = window.CONFIG_OFFICIAL_SHOP_ROOT_URL;
			var hashPart = parse_hash_general(targetUrl) || {};
			var path = targetUrl.split("#")[0];
			hashPart['init_position'] = initPosition;
			var landingPageUrl = generateHashUri(generateGetUri(path, params), hashPart);
			BJUtil.navigate(landingPageUrl, null, null, additionalConfig);
		}

	}

	function jumpToOfficialShopLanding(catid, initPosition) {
		catid = parseInt(catid);
		// In-browser logic.
		if (!isShopeeApp()){
			_jumpToOfficialShopLanding(catid, null, initPosition);
			return;
		}

		// In-app logic.
		if (BJUtil.isRNEnabled('official_shops')){
			BJUtil.navigateReactNative({
				module: "MALL_PAGE_LANDING",
				catid: catid,
				initPosition: initPosition,
			});
			return;
		}

		// If already loaded, use it.
		if (window.officialShopTabs && window.officialShopTabs.length > 0) {
			_jumpToOfficialShopLanding(catid, window.officialShopTabs, initPosition);
			return;
		}

		// Otherwise load first then use it.
		alert_message_with_loader();
		window.officialShopTabs = [];
		var ajaxObj = asyncGetOfficialShopTabs(window.officialShopTabs);
		ajaxObj.done(function () {
			_jumpToOfficialShopLanding(catid, window.officialShopTabs, initPosition);
		}).fail(function () {
			alert_message(i18n.t('label_unexpected_error'));
		}).always(function () {
			hide_message_with_loader();
		});
	}

	function jumpToBrandlist(catid, catname, initPosition) {
		if (isShopeeApp()) {
			if (!window.brandListTabs || window.brandListTabs.length == 0) {
				if (window.asyncGetBrandListTabs) {
					alert_message_with_loader();
					window.brandListTabs = [];
					var ajaxObj = asyncGetBrandListTabs(window.brandListTabs);
					ajaxObj.done(function () {
						_jumpToBrandList(catid, catname, window.brandListTabs);
					}).fail(function () {
						alert_message(i18n.t('label_unexpected_error'));
					}).always(function () {
						hide_message_with_loader();
					});
				}
			} else {
				_jumpToBrandList(catid, catname, window.brandListTabs);
			}
		} else {
			_jumpToBrandList(catid, catname, null);
		}
	}

	function _jumpToBrandList(catid, catname, tabs) {
		var additionalConfig = {};
		tabs = tabs || [];
		var tabIndex = _.map(tabs, function (tab) {
			return tab.catid;
		}).indexOf(catid);

		if (tabIndex < 0) {
			tabIndex = 0;
		}

		additionalConfig.navbar = getOfficialShopNavBar();
		delete additionalConfig.navbar.rightItemsConfig;
		if (tabs.length > 1) {
			additionalConfig.tabs = tabs;
			additionalConfig.tabsConfig = {
				textColor: '#212121', //disabled. use default color.
				activeColor: '#D0011B',
				barColor: '#FFFFFF',
				textSize: 14,
				textSizeActive: 16,
				tabIndex: tabIndex || 0,
			};
			additionalConfig.pageType = SEARCH_ENUM.PAGE_TYPE_ENUM.OFFICAL_SHOP_LANDING;
		}

		var params = {
			catid: catid || Constants.OFFICAL_SHOP_POPULAR_CATID,
			catname: catname,
		};
		if (tabs && tabs.length){
			params.tabs_count = tabs.length;
			BJUtil.navigate('', null, null, additionalConfig);
		} else {
			var targetUrl = generateGetUri(location.origin + '/mall/brands/' + catid + '/', params);
			BJUtil.navigate(targetUrl, null, null, additionalConfig);
		}
	}

	function isOfficialShopLandingPageUrl(url){
		url = url.replace('mall.', ''); // Because our official shop landing page path could be /mall
		var urlObj = urlToLocation(url);

		// offical shop landing page url if it is '/mall' or '/Official-Shops'
		if (urlObj.pathname === window.CONFIG_OFFICIAL_SHOP_ROOT_URL || urlObj.pathname === window.CONFIG_OFFICIAL_SHOP_ROOT_URL + '/'){
			return true
		}

		// must check tailing '/' to avoid mismatch of shop name like shopee.sg/mallcenter
		if (urlObj.pathname.indexOf(window.CONFIG_OFFICIAL_SHOP_ROOT_URL + '/') == 0){
			return true
		}

		if (urlObj.pathname.indexOf("/Official-Shops"  + '/') == 0){
			// 20180220 find SG configure url wrongly, will ask them to change
			return true
		}

		return false
	}

	function asyncGetOfficialShopTabs(tabs) {
		return getJSON('/api/v2/content/official_shop_landing_tabs', function(e) {
			if(!e || !e.data || !e.data.items) {
				return;
			}
			var items = e.data.items;
			for(var i=0; i<items.length; i++) {
				var item = items[i];
				var tab = {};
				var params = {
					catid: item.catid,
					diable_rn: true
				};
				tab.name = item.display_name;
				tab.url = generateGetUri(location.origin + window.CONFIG_OFFICIAL_SHOP_ROOT_URL +'/', params);
				tab.catid = item.catid;
				tabs.push(tab);
			}
		});
	}

	function needUpgradeAppForCancellation() {
		var ver = window.APPVER || window.VERSION;
		if ((isIOS()&&ver<21300)||(isAndroid()&&ver<21300)) {
			bridgeCallHandler('showPopUp', {
				popUp:{
					message:i18n.t('msg_cancel_order_upgrade'),
					okText:i18n.t('label_ok')
				}
			}, function(e) {
				bridgeCallHandler('popWebView');
			});
		}
	}

	var EVENT_DISMISS_ADULT = 'EVENT_DISMISS_ADULT';

	return {
		configurePageTitle: configurePageTitle,
		navigate: navigate,
		checkAdult: checkAdult,
		navigateModally: navigateModally,
		navigateHashTag: navigateHashTag,
		navigateCollection: navigateCollection,
		getCollectionNavBar: getCollectionNavBar,
		viewWillReappear: viewWillReappear,
		viewWillReappearHandler: viewWillReappearHandler,
		pop: pop,
		ajax: ajax,
		post: post,
		get: get,
		polling: polling,
		sentryLog: sentryLog,
		listenLike: listenLike,
		popUserPrivacy: popUserPrivacy,
		listenFollow: listenFollow,
		findHash: findHash,
		listenAnchor: listenAnchor,
		listenHash: listenHash,
		listenShop: listenShop,
		listenUser: listenUser,
		listenShopItems: listenShopItems,
		listenRates: listenRates,
		listenSnapshotItem: listenSnapshotItem,
		listenItem: listenItem,
		listenItemSimilar:listenItemSimilar,
		listenComment: listenComment,
		listenShare: listenShare,
		listenVideo: listenVideo,
		listenCategoryLink: listenCategoryLink,
		listenProductVideo: listenProductVideo,
		showWebPopUp: showWebPopUp,
		showWebPinPopUp: showWebPinPopUp,
		isV2: isV2,
		handleSearchHashtag: handleSearchHashtag,
		handleFollow: handleFollow,
		handleUnfollow: handleUnfollow,
		save: save,
		load: load,
		setupLoadMore: setupLoadMore,
		isInModal: function() {
			return !!latestFrameName;
		},
		isThisPageModal: function() {
			return !!parse_get_params(location.href).is_panel;
		},
		idx: idx,
		ownIdx: ownIdx,
		set: set,
		callFunction: callFunction,
		checkLogin: checkLogin,
		handleProtobufError: handleProtobufError,
		isSimpleVersion: isSimpleVersion,
		globalVersionFallback: globalVersionFallback,
		listenTicker: listenTicker,
		isAdultSafe: isAdultSafe,
		listenAge: listenAge,
		listenLastlogin: listenLastlogin,
		showActionSheet: showActionSheet,
		showDropdownMenu: showDropdownMenu,
		isInteger: isInteger,
		getShareIcon: getShareIcon,
		enterLocationMode: enterLocationMode,
		hideLocationBar: hideLocationBar,
		init: init,
		obtainLocation: obtainLocation,
		onLocationScroll: onLocationScroll,
		EVENT_DISMISS_ADULT: EVENT_DISMISS_ADULT,
		customSelectThenNavigate: customSelectThenNavigate,
		listenSearch: listenSearch,
		listenOrder: listenOrder,
		jumpToSearch: jumpToSearch,
		reInitPage: reInitPage,
		showMsgAndGoBack: showMsgAndGoBack,
		showMsg: showMsg,
		goBackOrClose: goBackOrClose,
		showSellerBlockedUserPopup: showSellerBlockedUserPopup,
		encryptPassword: encryptPassword,
		toggleGeneralMask: toggleGeneralMask,
		renderItemDisplayCard: renderItemDisplayCard,
		redirectToPC: redirectToPC,
		getPriceForDisplay: getPriceForDisplay,
		calculateLocationInAds: calculateLocationInAds,
		updateGeoInfo: updateGeoInfo,
		getTrackingReferUrls: getTrackingReferUrls,
		showPopupWithLearnMore: showPopupWithLearnMore,
		showConfirmPopup: showConfirmPopup,
		showPopup: showPopup,
		is_ads_topup_shopid:is_ads_topup_shopid,
		disablePageScroll: disablePageScroll,
		enablePageScroll: enablePageScroll,
		getIconUrl: getIconUrl,
		navigateReactNaive: navigateReactNative, // Keep it now due to an existed typo in others' branch.
		navigateReactNative: navigateReactNative,
		showVoiceOTPPopUp: showVoiceOTPPopUp,
		navigateToMap: navigateToMap,
		disableLogisticsChannel: disableLogisticsChannel,
		disableCodLogisticsChannel: disableCodLogisticsChannel,
		disableLogisticsChannelAndCodChannel: disableLogisticsChannelAndCodChannel,
		updateChannelStatus: updateChannelStatus,
		isRNEnabled: isRNEnabled,
		addFromTabFilterToUrl: addFromTabFilterToUrl,
		getIconNameWithRatio: getIconNameWithRatio,
		jumpToOfficialShopLanding: jumpToOfficialShopLanding,
		getJSONPlus: getJSONPlus,
		jumpToBrandlist: jumpToBrandlist,
		isOfficialShopLandingPageUrl: isOfficialShopLandingPageUrl,
		asyncGetOfficialShopTabs: asyncGetOfficialShopTabs,
		needUpgradeAppForCancellation: needUpgradeAppForCancellation,
	}
})();
