(function () {
	"use strict";
	// 時間格式處理
	if (!Date.pattern) {
		Date.prototype.pattern = function(fmt) {
			var o = {
				"M+" : this.getMonth()+1, //月份
				"d+" : this.getDate(), //日
				"h+" : this.getHours()%12 == 0 ? 12 : this.getHours()%12, //小时
				"H+" : this.getHours(), //小时
				"m+" : this.getMinutes(), //分
				"s+" : this.getSeconds(), //秒
				"q+" : Math.floor((this.getMonth()+3)/3), //季度
				"S"  : this.getMilliseconds() //毫秒
			};
			
			var week = {
				"0" : "/u65e5",
				"1" : "/u4e00",
				"2" : "/u4e8c",
				"3" : "/u4e09",
				"4" : "/u56db",
				"5" : "/u4e94",
				"6" : "/u516d"
			};
			
			if (/(y+)/.test(fmt)) {
				fmt = fmt.replace(RegExp.$1, (this.getFullYear()+"").substr(4 - RegExp.$1.length));
			}
			
			if (/(E+)/.test(fmt)) {
				fmt = fmt.replace(RegExp.$1, ((RegExp.$1.length>1) ? (RegExp.$1.length>2 ? "/u661f/u671f" : "/u5468") : "")+week[this.getDay()+""]);
			}
			
			for (var k in o) {
				if(new RegExp("("+ k +")").test(fmt)){
					fmt = fmt.replace(RegExp.$1, (RegExp.$1.length==1) ? (o[k]) : (("00"+ o[k]).substr((""+ o[k]).length)));
				}
			}
			
			return fmt;
		}
	}
	// 天數差處理
	if (!Date.dayDiff) {
		Date.prototype.dayDiff = function(timeStr) {
			var that = (new Date (timeStr));
			var days = Math.floor(Math.abs(this.getTime() - that.getTime()) / (24 * 60* 60 * 1000));
			return this.getTime() >= that.getTime() ? days : days * (-1);
		}
	}
	// 左邊補0
	if (!String.padLeftZero) {
		String.prototype.padLeftZero = function (length) {
			return (Array(length).join('0') + this).slice(-length);
		}
	}
	// font-size => fontSize
	if (!String.MinusToHump) {
		String.prototype.MinusToHump = function () {
			return this.replace(/\-(\w)/g, function(all, letter){
				return letter.toUpperCase();
			});
		}
	}
	// fontSize => font-size
	if (!String.HumpToMinus) {
		String.prototype.HumpToMinus = function () {
			return this.replace(/([A-Z])/g,"-$1").toLowerCase();
		}
	}
	// 取得第一個
	if (!Array.first) {
		Array.prototype.first = function () {
			return this[0] || null;
		};
		// Hide method from for-in loops
		Object.defineProperty(Array.prototype, "first", { enumerable: false });
	};
	// 取得最後一個
	if (!Array.last) {
		Array.prototype.last = function () {
			return this.length > 0 ? this[this.length - 1] : null;
		};
		// Hide method from for-in loops
		Object.defineProperty(Array.prototype, "last", { enumerable: false });
	};
	// 氣泡排序
	if (!Array.bubbleSort) { 
		Array.prototype.bubbleSort = function () {
			var arr = [];
			
			if (this == null) {
				throw new TypeError('this is null or not defined');
			}
			
			if (this.length > 0) {
				
				var self = this;
				var num = undefined;
				
				while( (num = self.shift()) !== undefined ) {
					if (isNaN(num)) {
						continue;
					}
					
					num = Number(num);
					if (!arr.length) {
						arr.push(num);
						continue;
					}
					
					var len = arr.length;
					for (var i=0; i < len; i++) {
						if (num >= arr[i]) {
							arr.splice(i, 0, num);
							break;
						}
					}
					
					if (len == arr.length)
						arr.push(num);
				}
				
			}
			
			return arr;
		}
		// Hide method from for-in loops
		Object.defineProperty(Array.prototype, "bubbleSort", { enumerable: false });
	}
	
	if (!Array.each) { 
		Array.prototype.each = function(fn){
		  fn = fn || Function.K;
		   var a = [];
		   var args = Array.prototype.slice.call(arguments, 1);
		   for(var i = 0; i < this.length; i++){
			   var res = fn.apply(this,[this[i],i].concat(args));
			   if(res != null) a.push(res);
		   }
		   return a;
		};
		// Hide method from for-in loops
		Object.defineProperty(Array.prototype, "each", { enumerable: false });
	} 
	// 數組是否包含指定元素
	if (!Array.contains) { 
		Array.prototype.contains = function(suArr){
		  for(var i = 0; i < this.length; i ++){
			  if(this[i] == suArr){
				  return true;
			  }
		   }
		   return false;
		}
		// Hide method from for-in loops
		Object.defineProperty(Array.prototype, "contains", { enumerable: false });
	} 
	// 不重複數組
	if (!Array.uniquelize) { 
		Array.prototype.uniquelize = function(){
		   var ra = new Array();
		   for(var i = 0; i < this.length; i ++){
			  if(!ra.contains(this[i])){
				  ra.push(this[i]);
			  }
		   }
		   return ra;
		};
		// Hide method from for-in loops
		Object.defineProperty(Array.prototype, "uniquelize", { enumerable: false });
	}
	// 物件新增 forEach 支援
	if (!Object.prototype.forEach) {
		Object.defineProperty(Object.prototype, "forEach", {
			value: function (callback, thisArg) {
				if (this == null) {
					throw new TypeError("Not an object");
				}
				thisArg = thisArg || window;
				for (var key in this) {
					if (this.hasOwnProperty(key)) {
						callback.call(thisArg, this[key], key, this);
					}
				}
			},
			configurable: true // 引入googlemap會衝突 所以引入時會自動移除此支援
		});
	}
	//axios retry 設置
	if (axios.defaults) {
		axios.defaults.retry = 5;
		axios.defaults.retryDelay = 3000;
		axios.defaults.timeout = 5000;
		axios.defaults.retryStatus = [];
		// retry 機制只針對 timeout 有需要指定的狀態 retry 請定義
		axios.interceptors.response.use(undefined, function axiosRetryInterceptor(err) {
			var config = err.config;
			// If config does not exist or the retry option is not set, reject
			if(!config || !config.retry || 
			  (err.code !== "ECONNABORTED" && (typeof err.response == "undefined" || !Object.values(config.retryStatus).includes(err.response.status)))
			) { 
				return Promise.reject(err); 
			}
			// Set the variable for keeping track of the retry count
			config.__retryCount = config.__retryCount || 0;
			// Check if we've maxed out the total number of retries
			if(config.__retryCount >= config.retry) {
				// Reject with the error
				return Promise.reject(err);
			}
			// Increase the retry count
			config.__retryCount += 1;
			// Create new promise to handle exponential backoff
			var backoff = new Promise(function(resolve) {
				setTimeout(function() {
					resolve();
				}, config.retryDelay || 1);
			});
			// Return the promise in which recalls axios to retry the request
			return backoff.then(function() {
				return axios(config);
			});
		});
	}
}());

// 針對 web storage 相關處理的擴充庫
(function (root, factory) {
	if (typeof define === 'function' && define.amd) {
		define(factory);
	} else if (typeof exports === 'object') {
		module.exports = factory();
	} else {
		root.WebStorageCache = factory();
	}
}(this, function () {
	"use strict";

	var _maxExpireDate = new Date('Fri, 31 Dec 9999 23:59:59 UTC');
	var _defaultExpire = _maxExpireDate;

	// https://github.com/jeromegn/Backbone.localStorage/blob/master/backbone.localStorage.js#L63
	var defaultSerializer = {
		serialize: function (item) {
			return JSON.stringify(item);
		},
		// fix for "illegal access" error on Android when JSON.parse is
		// passed null
		deserialize: function (data) {
			return data && JSON.parse(data);
		}
	};

	function _extend (obj, props) {
		for (var key in props) obj[key] = props[key];
		return obj;
	}
	
	// https://github.com/gsklee/ngStorage/blob/master/ngStorage.js#L52
	// When Safari (OS X or iOS) is in private browsing mode, it appears as
	// though localStorage is available, but trying to call .setItem throws an
	// exception below: "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was
	// made to add something to storage that exceeded the quota.
	function _isStorageSupported (storage) {
		var supported = false;
		if (storage && storage.setItem ) {
			supported = true;
			var key = '__' + Math.round(Math.random() * 1e7);
			try {
				storage.setItem(key, key);
				storage.removeItem(key);
			} catch (err) {
				supported = false;
			}
		}
		return supported;
	}

	// get storage instance
	function _getStorageInstance (storage) {
		var type = typeof storage;
		if (type === 'string' && window[storage] instanceof Storage) {
			return window[storage];
		}
		return storage;
	}

	function _isValidDate (date) {
		return Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date.getTime());
	}

	function _getExpiresDate (expires, now) {
		now = now || new Date();

		if (typeof expires === 'number') {
			expires = expires === Infinity ?
			_maxExpireDate : new Date(now.getTime() + expires * 1000);
		} else if (typeof expires === 'string') {
			expires = new Date(expires);
		}

		if (expires && !_isValidDate(expires)) {
			throw new Error('`expires` parameter cannot be converted to a valid Date instance');
		}

		return expires;
	}

	// http://crocodillon.com/blog/always-catch-localstorage-security-and-quota-exceeded-errors
	function _isQuotaExceeded(e) {
		var quotaExceeded = false;
		if (e) {
			if (e.code) {
				switch (e.code) {
					case 22:
					quotaExceeded = true;
					break;
					case 1014:
					// Firefox
					if (e.name === 'NS_ERROR_DOM_QUOTA_REACHED') {
						quotaExceeded = true;
					}
					break;
				}
			} else if (e.number === -2147024882) {
				// Internet Explorer 8
				quotaExceeded = true;
			}
		}
		return quotaExceeded;
	}

	// cache item constructor
	function CacheItemConstructor (value, exp) {
		// createTime
		this.c = (new Date()).getTime();
		exp = exp || _defaultExpire;
		var expires = _getExpiresDate(exp);
		// expiresTime
		this.e = expires.getTime();
		this.v = value;
	}

	function _isCacheItem(item) {
		if (typeof item !== 'object') {
			return false;
		}
		if(item) {
			if('c' in item && 'e' in item && 'v' in item) {
				return true;
			}
		}
		return false;
	}

	// check cacheItem If effective
	function _checkCacheItemIfEffective(cacheItem) {
		var timeNow = (new Date()).getTime();
		return timeNow < cacheItem.e;
	}

	function _checkAndWrapKeyAsString(key) {
		if (typeof key !== 'string') {
			console.warn(key + ' used as a key, but it is not a string.');
			key = String(key);
		}
		return key;
	}

	// cache api
	var CacheAPI = {

		set: function (key, value, options) {},

		get: function (key) {},

		delete: function (key) {},
		// Try the best to clean All expires CacheItem.
		deleteAllExpires: function() {},
		// Clear all keys
		clear: function () {},
		// Add key-value item to memcached, success only when the key is not exists in memcached.
		add: function (key, options) {},
		// Replace the key's data item in cache, success only when the key's data item is exists in cache.
		replace: function (key, value, options) {},
		// Set a new options for an existing key.
		touch: function (key, exp) {}
	};

	// cache api
	var CacheAPIImpl = {

		set: function(key, val, options) {

			key = _checkAndWrapKeyAsString(key);

			options = _extend({force: true}, options);

			if (val === undefined) {
				return this.delete(key);
			}

			var value = defaultSerializer.serialize(val);

			var cacheItem = new CacheItemConstructor(value, options.exp);
			try {
				this.storage.setItem(key, defaultSerializer.serialize(cacheItem));
			} catch (e) {
				if (_isQuotaExceeded(e)) { //data wasn't successfully saved due to quota exceed so throw an error
					this.quotaExceedHandler(key, value, options, e);
				} else {
					console.error(e);
				}
			}

			return val;
		},
		
		get: function (key) {
			key = _checkAndWrapKeyAsString(key);
			var cacheItem = null;
			try{
				cacheItem = defaultSerializer.deserialize(this.storage.getItem(key));
			}catch(e){
				return null;
			}
			if(_isCacheItem(cacheItem)){
				if(_checkCacheItemIfEffective(cacheItem)) {
					var value = cacheItem.v;
					return defaultSerializer.deserialize(value);
				} else {
					this.delete(key);
				}
			}
			return null;
		},

		delete: function (key) {
			key = _checkAndWrapKeyAsString(key);
			this.storage.removeItem(key);
			return key;
		},

		deleteAllExpires: function() {
			var length = this.storage.length;
			var deleteKeys = [];
			var _this = this;
			for (var i = 0; i < length; i++) {
				var key = this.storage.key(i);
				var cacheItem = null;
				try {
					cacheItem = defaultSerializer.deserialize(this.storage.getItem(key));
				} catch (e) {}

				if(cacheItem !== null && cacheItem.e !== undefined) {
					var timeNow = (new Date()).getTime();
					if(timeNow >= cacheItem.e) {
						deleteKeys.push(key);
					}
				}
			}
			deleteKeys.forEach(function(key) {
				_this.delete(key);
			});
			return deleteKeys;
		},

		clear: function () {
			this.storage.clear();
		},

		add: function (key, value, options) {
			key = _checkAndWrapKeyAsString(key);
			options = _extend({force: true}, options);
			try {
				var cacheItem = defaultSerializer.deserialize(this.storage.getItem(key));
				if (!_isCacheItem(cacheItem) || !_checkCacheItemIfEffective(cacheItem)) {
					this.set(key, value, options);
					return true;
				}
			} catch (e) {
				this.set(key, value, options);
				return true;
			}
			return false;
		},

		replace: function (key, value, options) {
			key = _checkAndWrapKeyAsString(key);
			var cacheItem = null;
			try{
				cacheItem = defaultSerializer.deserialize(this.storage.getItem(key));
			}catch(e){
				return false;
			}
			if(_isCacheItem(cacheItem)){
				if(_checkCacheItemIfEffective(cacheItem)) {
					this.set(key, value, options);
					return true;
				} else {
					this.delete(key);
				}
			}
			return false;
		},

		touch: function (key, exp) {
			key = _checkAndWrapKeyAsString(key);
			var cacheItem = null;
			try{
				cacheItem = defaultSerializer.deserialize(this.storage.getItem(key));
			}catch(e){
				return false;
			}
			if(_isCacheItem(cacheItem)){
				if(_checkCacheItemIfEffective(cacheItem)) {
					this.set(key, this.get(key), {exp: exp});
					return true;
				} else {
					this.delete(key);
				}
			}
			return false;
		}
	};
	
	// Cache Constructor
	function CacheConstructor (options) {

		// default options
		var defaults = {
			storage: 'localStorage',
			exp: Infinity  //An expiration time, in seconds. default never .
		};

		var opt = _extend(defaults, options);

		var expires = opt.exp;

		if (expires && typeof expires !== 'number' && !_isValidDate(expires)) {
			throw new Error('Constructor `exp` parameter cannot be converted to a valid Date instance');
		} else {
			_defaultExpire = expires;
		}

		var storage = _getStorageInstance(opt.storage);

		var isSupported = _isStorageSupported(storage);

		this.isSupported = function () {
			return isSupported;
		};

		if (isSupported) {

			this.storage = storage;

			this.quotaExceedHandler = function (key, val, options, e) {
				console.warn('Quota exceeded!');
				if (options && options.force === true) {
					var deleteKeys = this.deleteAllExpires();
					console.warn('delete all expires CacheItem : [' + deleteKeys + '] and try execute `set` method again!');
					try {
						options.force = false;
						this.set(key, val, options);
					} catch (err) {
						console.warn(err);
					}
				}
			};

		} else {  // if not support, rewrite all functions without doing anything
			_extend(this, CacheAPI);
		}

	}

	CacheConstructor.prototype = CacheAPIImpl;

	return CacheConstructor;

}));

// 兩個數組的交集
Array.intersect = function(a, b){
	return a.uniquelize().each(function(o){return b.contains(o) ? o : null});
};
 
// 兩個數組的差集
Array.minus = function(a, b) {
	return a.uniquelize().each(function(o){return b.contains(o) ? null : o});
};
 
// 兩個數組的補集
Array.complement = function(a, b) {
	return Array.minus(Array.union(a, b), Array.intersect(a, b));
};
 
// 兩個數組的聯集
Array.union = function(a, b) {
	return a.concat(b).uniquelize();
};

function basename(str, sep) {
	sep = sep || "/";
    return str.substr(str.lastIndexOf(sep) + 1);
}

function strip_extension(str) {
    return str.lastIndexOf(".") != -1 ? str.substr(0, str.lastIndexOf(".")) : str;
}

function js_uuid() {
  var d = Date.now();
  if (typeof performance !== 'undefined' && typeof performance.now === 'function'){
    d += performance.now(); //use high-precision timer if available
  }
  return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
    var r = (d + Math.random() * 16) % 16 | 0;
    d = Math.floor(d / 16);
      return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
  });
}

function delay(milliseconds) {
    return new Promise(resolve => {
        setTimeout(resolve, milliseconds);
    });
}

function num_round(num, decimal) {
	if (typeof num != "number") {
		if (isNaN(Number(num))) {
			return 0;
		}
		num = Number(num);
	}
	if (typeof decimal != "number" || !/^\d+$/.test(decimal))
		decimal = 2;
    return +(Math.round(num + `e+${decimal}`)  + `e-${decimal}`);
}