var wctx = {
	path:"",
	url:function(path) {
		return this.path + path;
	},
	home:function() {
		top.location.href = wctx.url("/");
	},
	current:function() {
		let str = top.location.href.replace(top.location.origin + wctx.path, "");
		let pos = str.indexOf("?");
		return pos < 0 ? str : str.substr(0, pos);
	},
	trace:true
};

function log(msg) {
	console.log.apply(console, arguments);
}

function debug(msg) {
	if (wctx && wctx.trace)
		console.log.apply(console, arguments);
}

function trim(s) {
	if (null == s || undefined == s) return "";
	if ("string" != typeof(s)) return s;
	return s.replace(/^\s+/, "").replace(/\s+$/, "");
}

function isEmpty(v) {
	if (v == undefined
	 || v == "undefined"
	 || v == null
	 || v == "null"
	) return true;

	switch (typeof(v)) {
	case "string": return "" == trim(v);
	case "boolean": if (false == v) return false;
	case "number": if (0 == v) return false;
	default: return false;
	}
}

function ifEmpty(v, nv) {
	if (!isEmpty(v)) return v;
	if (typeof(nv) == "function")
		return nv.apply();
	else
		return nv;
}

function notEmpty(obj, msg) {
	if (isEmpty(obj))
		throw msg;
	return obj;
}

function toQuery(map, encode) {
	if (isEmpty(map)) return "";

	var query = [];
	for (var key in map) {
		var v = map[key];
		if (v != null && v == undefined) continue;
		if (v != null)
		switch (typeof(v)) {
		case "object":
		case "function": continue;
		}
		if (isEmpty(v))
			query.push(key);
		else
			query.push(key + "=" + (encode != false ? encodeURIComponent(v) : v));
	}
	return query.join("&");
}

function uuid() {
	var hexDigits = "0123456789abcde",
		result = [];

	for (var i = 0; i < 36; ++i)
		result[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
	result[14] = "4";
	result[19] = hexDigits.substr((result[19] & 0x3) | 0x8, 1);
	result[8] = result[13] = result[18] = result[23] = "-";

	return result.join("");
}

function wait(show) {
	if (show == false)
		$(".wait").hide();
	else
		$(".wait").show();
}

var dialog = {
	title:"XIT",
	template:null,
	open:function(conf) {
		if (this.template)
			this.create(conf);
		else {
			var self = this;
			ajax.get({
				url:wctx.url("/resources/html/dialog.html"),
				success:function(resp) {
					self.template = resp;
					self.create(conf);
				}
			});
		}
	},
	close:function(id) {
		$("#close" + id).click();
	},
	create:function(conf) {
		conf = conf || {};
		let id = conf.id || "dlg-" + uuid(),
			backdropID = id + "-backdrop",
			size = conf.size || "",
			tmpl = this.template
				.replace(/{id}/g, id)
				.replace(/{title}/g, conf.title || dialog.title);
			if (size)
				size = " modal-" + size;
			tmpl = tmpl.replace(/{size}/, size);
			dlg = $(tmpl).appendTo("body");

		dlg.find(".modal-body").html(conf.content || "").fadeIn();

		dlg.on("hidden.bs.modal", function() {// on dialog close
			$("#" + id +",#" + backdropID).remove(); // removes the dialog and its backdrop
			if (conf.onClose)
				conf.onClose();
		});

		if (conf.onOK) {
			let footer = dlg.find(".modal-footer");
			footer.show();
			footer.find("button").unbind("click").click(function() {
				if (conf.getData) {
					var selected = conf.getData.apply();
					if (!selected) return;

					conf.onOK(selected);
					dialog.close(id);
				} else {
					conf.onOK();
					dialog.close(id);
				}
			});
		} else {
			if (conf.timeout)
				setTimeout(function(){dialog.close(id);}, conf.timeout);
		}

		dlg.draggable().modal("show");
		$(".modal-backdrop").each(function() { // gives id to its backdrop
			let backdrop = $(this);
			if (!backdrop.prop("id"))
				backdrop.prop("id", backdropID);
		});

		if (conf.init)
			conf.init();
	},
	alert:function(conf) {
		var container = "<div class='container-fluid text-center' style='font-size:1.4em;'>{content}</div>";
		if ("string" == typeof(conf)) {
			conf = {
				content:container.replace(/{content}/g, conf)
			};
		} else {
			conf.content = container.replace(/{content}/g, conf.content);
		}
		conf.timeout = ifEmpty(conf.timeout, 2000);
		this.open(conf);
	}
};

function onError(xhr, options, error) {
	if (xhr.readyState == 0)
		return dialog.alert("서버에 접근할 수 없습니다.");

	var resp = JSON.parse(xhr.responseText);
	if (resp.handler)
		return eval(resp.handler);

	var	msgs = [];
	for (key in resp)
		msgs.push(resp[key])
	msgs = msgs.join("<br />");
	if (msgs)
		dialog.alert(msgs);
}

var ajax = {
	request:function(options) {
		options.beforeSend = function(xhr) {
			if (wctx.csrf)
				xhr.setRequestHeader(wctx.csrf.header, wctx.csrf.token);
			if (!options.silent)
				wait();
		}
		if (!options.type) {
			if (options.data)
				options.type = "POST";
		}

		var success = options.success;
		options.success = function(resp) {
			if ("string" == typeof resp)
				resp = trim(resp);

			var stacktrace = resp.stacktrace;
			delete resp.stacktrace;

			debug("response", resp);

			if (!resp.failed)
				return success(resp);

			dialog.alert({
				title:resp.title,
				content:[resp.description, resp.message].join("<br />")
			});

			debug("stacktrace", stacktrace);
		};
		var handleError = options.error || onError;
		options.error = function(xhr, options, error) {
			wait(false);

			debug("error", xhr, options, error);
			handleError(xhr, options, error);
		}

		var handleComplete = options.complete || function(){};
		options.complete = function() {
			wait(false);
			handleComplete();
		};
		debug("request", options);
		$.ajax(options);
	},
	get:function(options) {
		options.type = "GET";
		ajax.request(options);
	},
	post:function(options) {
		options.type = "POST";
		ajax.request(options);
	}
};

var json = {
	with:function(options) {
		options.dataType = "json";
		if (typeof(options.data) == "string") {
			options.contentType = "application/json; charset=UTF-8";
		}
		return options;
	},
	get:function(options) {
		ajax.get(json.with(options));
	},
	post:function(options) {
		ajax.post(json.with(options));
	}
};

function upload(options) {
	options.enctype = "multipart/form-data";
	options.processData = options.contentType = false;
	var data = options.data,
		formData = new FormData();
	for (var key in data) {
		var val = data[key];
		if (!Array.isArray(val))
			formData.append(key, val);
		else {
			for (var i = 0; i < val.length; ++i)
				formData.append(key, val[i]);
		}
	}
	options.data = formData;
	ajax.post(options);
}

$.fn.onEnterPress = function(handler) {
	return this.each(function(){
		$(this).keypress(function(evt){
			if (!handler || evt.which != 13) return;
			handler.apply();
		});
	});
}

function labelFor(input) {
	let selector = "label[for='{id}']",
		str = "string" == typeof(input),
		obj = $(input),
		key = str ? input
			: obj.attr("id") || obj.attr("name");
	if (!key) return "";

	selector = selector.replace(/{id}/gi, key);
	return $(selector).text()
		|| obj.attr("title")
		|| obj.attr("placeholder");
}

function validationFailureHandler() {
	var handler = {
			notice:function(msg, input) {
				dialog.alert({
					content:msg,
					onClose:function(){input.focus();}
				});
			},
			valueMissing: function(input) {
				handler.notice(labelFor(input) + "를(을) 입력하십시오.", input);
			},
			typeMismatch: function(input) {
				handler.notice(input.value + "는 " + labelFor(input) + "의 형식에 맞지 않습니다.", input);
			},
			tooLong: function(input) {
				handler.notice(input.value + " 값이 너무 깁니다.", input);
			},
			patternMismatch: function(input) {
				handler.notice(input.value + "는 " + labelFor(input) + "의 형식에 맞지 않습니다.", input);
			},
			rangeOverflow: function(input) {
				handler.notice(labelFor(input) + "의 값은 " + input.max + "보다 작아야 합니다.", input);
			},
			rangeUnderflow: function(input) {
				handler.notice(labelFor(input) + "의 값은 " + input.min + "보다 커야 합니다.", input);
			},
			stepMismatch: function(input) {
				handler.notice(labelFor(input) + "의 값은 " + input.step + "씩 증가 또는 감소해야 합니다.", input);
			}
		};
	return handler;
}

/**Returns the result of validation on the input object.<br />
 * The types of validation checks are those defined by input objects' basic validity property, which are
 * <ul><li>valueMissing</li>
 * 	   <li>typeMismatch</li>
 * 	   <li>tooLong</li>
 * 	   <li>patternMismatch</li>
 * 	   <li>rangeOverflow</li>
 * 	   <li>rangeUnderflow</li>
 * 	   <li>stepMismatch</li>
 * </ul>
 * To be effective, the input object must be set with attributes
 * <ul><li>required</li>
 * 	   <li>type</li>
 * 	   <li>maxLength</li>
 * 	   <li>pattern</li>
 * 	   <li>max</li>
 * 	   <li>min</li>
 * 	   <li>step</li>
 * </ul>
 * On failure, failureHandler takes over.<br />
 * The default handler from <code>validationFailureHandler()</code> displays a message
 * and puts the focus back to the input object.<br />
 * For the handler to display a message appropriate for the failure,
 * it is recommended to have a label associated with the input object.
 * @param input input object
 * @param failureHandler object that handles validation failure.<br />
 * 		  If not provided, the default handler from <code>validationFailureHandler()</code> is used.
 * @returns
 * <ul><li>true if the value of the input object is valid</li>
 * 	   <li>false otherwise</li>
 * </ul>
 */
function validInput(input, failureHandler) {
	var validity = input.validity;
	if (!validity || validity.valid) return true;

	for (var key in validity) {
		if (!validity[key]) continue;

		if (!failureHandler)
			failureHandler = validationFailureHandler();
		var handler = failureHandler[key];
		if (handler)
			handler(input);
		else {
			log("Handler not found for validation failure of " + key);
		}
		break;
	}

	return false;
}

/**Returns the result of input validation on the selected input objects.<br />
 * The types of validation checks are those defined by input objects' basic validity property, which are
 * <ul><li>valueMissing</li>
 * 	   <li>typeMismatch</li>
 * 	   <li>tooLong</li>
 * 	   <li>patternMismatch</li>
 * 	   <li>rangeOverflow</li>
 * 	   <li>rangeUnderflow</li>
 * 	   <li>stepMismatch</li>
 * </ul>
 * To be effective, the input objects must be set with attributes
 * <ul><li>required</li>
 * 	   <li>type</li>
 * 	   <li>maxLength</li>
 * 	   <li>pattern</li>
 * 	   <li>max</li>
 * 	   <li>min</li>
 * 	   <li>step</li>
 * </ul>
 * On failure, failureHandler takes over.<br />
 * The default handler from <code>validationFailureHandler()</code> displays a message
 * and puts the focus back to the offending input object.<br />
 * For the handler to display a message appropriate for the failure,
 * it is recommended to have labels associated with input objects.
 * @param failureHandler object that handles validation failure.<br />
 * 		  If not provided, the default handler from <code>validationFailureHandler()</code> is used.
 * @returns
 * <ul><li>true if the input values of the selected objects are valid</li>
 * 	   <li>false otherwise</li>
 * </ul>
 */
$.fn.validInputs = function(failureHandler) {
	var valid = true;
	this.each(function(){
		var input = $(this);
		if (!(valid = validInput(input[0], failureHandler))) {
			return false;
		}
	});
	return valid;
}

$.fn.getValues = function(propertyMapper) {
	var inputValues = {};
	this.each(function() {
		var input = $(this),
			key = input.prop("id") || input.prop("name"),
			value = input.val();
		if (isEmpty(key) || value === undefined) return;

		var type = input.prop("type");
		if ("file" == type) {
			value = input.get(0).files;
		} else if (["checkbox", "radio"].indexOf(type) > -1) {
			if (!input.is(":checked")) return;
		}

		var stored = inputValues[key];
		if (stored === undefined) {
			inputValues[key] = value;
			return;
		}

		if (Array.isArray(stored)) {
			if (!Array.isArray(value))
				stored.push(value);
			else
				stored = stored.concat(value);
		} else {
			var array = [stored];
			if (!Array.isArray(value))
				array.push(value);
			else
				array = array.concat(value);
			inputValues[key] = array;
		}
	});

	if (propertyMapper) { // propertyMapper = {property:valueProvider, ...}
		for (var property in propertyMapper) {
			var valueProvider = propertyMapper[property],
				value = undefined;
			switch (typeof(valueProvider)) {
			case "string":
				value = inputValues[valueProvider];
				break;
			case "function":
				value = valueProvider(inputValues);
				break;
			}
			if (value === undefined) continue;

			delete inputValues[valueProvider];
			inputValues[property] = value;
		}
	}

	return inputValues;
}

/**
 * @param config
 * {start:0,
 *  fetchSize:10,
 *  totalSize:135,
 *  links:3,
 *  first:function(index, label){return "..."},
 *  previous:function(index, label){return "..."},
 *  link:function(index, label){return "..."},
 *  current:function(index, label){return "..."},
 *  next:function(index, label){return "..."},
 *  last:function(index, label){return "..."}
 * }
 * @returns {String}
 */
function paginate(config) {
	var rc = config.totalSize || 0;
//	if (!rc) return "";

	var	fetchCount = config.fetchSize || 0;
//	if (!fetchCount) return "";

	var fetch = {
		all:0,
		none:-1,
		count:function(elementCount, size) {
			if (!elementCount || size == fetch.all) return 1;
			return parseInt((elementCount / size) + ((elementCount % size) == 0 ? 0 : 1));
		},
		end:function(elementCount, size, start) {
			if (size < fetch.all) throw "Invalid size: " + size;
			if (elementCount < 0) throw "Invalid elementCount: " + elementCount;
			var last = elementCount - 1;
			if (size == fetch.all) return last;
			return Math.min(last, start + size -1);
		},
		page:function(current, count) {
			return parseInt(count < 1 ? 0 : current / count);
		},
		band:function(page, visibleLinks) {
			return parseInt(visibleLinks < 1 ? 0 : page / visibleLinks);
		}
	};
	var lc = fetch.count(rc, fetchCount);
	if (lc < 2) return "";

	var links = ifEmpty(config.links, fetch.all),
		page = fetch.page(ifEmpty(config.start, 0), fetchCount),
		band = fetch.band(page, links),
		tags = {
			link:function(tag, index, label) {
				return !tag ? "" : tag(index, label);
			},
			first:function() {
				return band < 2 ? "" : tags.link(config.first, 0, 1);
			},
			previous:function() {
				if (band < 1) return "";
			    var prevBand = band - 1,
					prevPage = (prevBand * links) + (links - 1),
			        fromRec = prevPage * fetchCount;
			    return tags.link(config.previous, fromRec, prevPage + 1);
			},
			visibleLinks:function() {
				var s = "",
					fromPage = links == fetch.all ? 0 : band * links,
					toPage = links == fetch.all ? lc : Math.min(lc, fromPage + links);
				for (var i = fromPage; i < toPage; ++i) {
					var fromRec = i * fetchCount,
						label = i + 1;
					s += tags.link(i == page ? config.current : config.link, fromRec, label);
				}
				return s;
			},
			next:function(bandCount) {
				bandCount = parseInt(bandCount);
				if (bandCount - band < 2) return "";

				var nextBand = band + 1,
					page = nextBand * links,
					fromRec = page * fetchCount;
				return tags.link(config.next, fromRec, page + 1);
			},
			last:function(bandCount) {
				bandCount = parseInt(bandCount);
			    var lastBand = bandCount - 1;
			    if (lastBand - band < 2) return "";

			    var pages = lastBand * links,
			        fromRec = pages * fetchCount;
				return tags.link(config.last, fromRec, pages + 1);
			}
		},
		tag = "";
	if (links != fetch.all) {
		tag += tags.first();
		tag += tags.previous();
	}
	tag += tags.visibleLinks();
	if (links != fetch.all) {
        var bandCount = parseInt(lc / links);
        bandCount += lc % links == 0 ? 0 : 1;
		tag += tags.next(bandCount);
		tag += tags.last(bandCount);
	}
	return tag;
}

$.fn.paginate = function(config) {
	return this.each(function(){
		var tag = paginate(config),
			container = $(this);
		if (tag)
			container.html(tag).show();
		else {
			if (config.hideIfEmpty != false)
				container.hide();
		}
	});
}

$.fn.setPaging = function(config) {
	config.links = 5;
	config.first = function(index, label) {return '<li onclick="{func};" class="page-item first"><a class="page-link"><i class="tf-icon bx bx-chevrons-left"></i></a></li>'.replace(/{func}/, config.func.replace(/{index}/, label));};
	config.previous = function(index, label) {return '<li onclick="{func}" class="page-item prev"><a class="page-link"><i class="tf-icon bx bx-chevron-left"></i></a></li>'.replace(/{func}/, config.func.replace(/{index}/, label));};
	config.link = function(index, label) {return '<li onclick="{func}" class="page-item"><a class="page-link">{label}</a></li>'.replace(/{func}/, config.func.replace(/{index}/, label)).replace(/{label}/, label);};
	config.current = function(index, label) {return '<li class="page-item active"><a class="page-link">{label}</a></li>'.replace(/{label}/, label);};
	config.next = function(index, label) {return '<li onclick="{func}" class="page-item next"><a class="page-link"><i class="tf-icon bx bx-chevron-right"></i></a></li>'.replace(/{func}/, config.func.replace(/{index}/, label));};
	config.last = function(index, label) {return '<li onclick="{func}" class="page-item last"><a class="page-link"><i class="tf-icon bx bx-chevrons-right"></i></a></li>'.replace(/{func}/, config.func.replace(/{index}/, label));};

	return this.each(function(){
		let list = config.list;
		let pagingInfo = list.empty ? "" : 1
		  + " ~ "
		  + numberFormat.format(list.length)
		  + " / " + numberFormat.format(config.totalSize);
		$("#"+ config.prefix + "PagingInfo").html(pagingInfo);

		let tag = paginate(config),
			container = $(this);
		if (tag)
			container.html(tag.replace(/{func}/g, config.func)).show();
		else {
			if (config.hideIfEmpty != false)
				container.hide();
		}

	});
}

$.fn.setPagingInfo = function(config) {
	return this.each(function(){
		let list = config.list;
		let pagingInfo = list.empty ? "" : 1
		  + " ~ "
		  + numberFormat.format(list.length)
		  + " / " + numberFormat.format(config.totalSize);
		$("#"+ config.prefix + "PagingInfo").html(pagingInfo);
	});
}

$.fn.setCurrentRow = function(val) {
	if (!val) return;

	return this.each(function() {
		var e = $(this);
		e.find("tr").each(function(){
			var tr = $(this),
				current = val == tr.attr("data-key");
			if (current)
				tr.addClass("current-row");
			else
				tr.removeClass("current-row");
		});
	});
}

class FormFields {
	constructor(selector) {
		this.form = document.querySelector(this.selector = selector);
		this.children = ["input", "select", "textarea"].map(tag => this.selector + " " + tag).join(",");
		this.datamap = [];
	}

	set(obj) {
		document.querySelectorAll(this.children).forEach(input => {
			let prop = input.getAttribute("data-map")
					|| input.name
					|| input.id;
			if (!prop) return;

			let dataItem = obj instanceof DataItem,
				value = dataItem ? obj.getValue(prop) : obj[prop];

			if ("radio" == input.type) {
				if (value && value == input.value)
					input.checked = true;
			} else {
				input.value = ifEmpty(value, "");
			}
		});
	}

	get() {
		let obj = {};
		document.querySelectorAll(this.children).forEach(input => {
			let property = input.name || input.id;
			let value = input.value;
			if ("radio" == input.type) {
				if (input.checked)
					obj[property] = value;
			} else {
				let dataFmtType = input.getAttribute("data-fmt-type");
				let dataMap = input.getAttribute("data-map");

				if (["day", "time"].includes(dataFmtType))
					obj[property] = value.replace(/-|:/gi, "");
				else if (dataMap != null && dataMap.endsWith("AMT"))
					obj[property] = value.replace(/,/gi, "");
				else
					obj[property] = value;
			}
//			log(property, value, "radio" == input.type ? "radio" : "", input.checked ? "checked" : "");
		});
		return obj;
		//return Object.fromEntries(new FormData(this.form).entries());
	}
};

/*
$.datepicker.setDefaults({
	dateFormat:"yy-mm-dd",
	yearSuffix:"년",
	monthNames:["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"],
	dayNames:["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"],
	dayNamesMin:["일", "월", "화", "수", "목", "금", "토"],
	showMonthAfterYear:true,
	showOtherMonths:true,
	selectOtherMonths:true,
	nextText:"다음 달",
	prevText:"이전 달"
});
*/
/**
 * @param fromSource 시작값을 갖는 input의 selector
 * @param toSource 종료값을 갖는 input의 selector
 */
function inputsInRange(fromSource, toSource) {
	var from = $(fromSource),
		to = $(toSource),
		compare = function() {
			var fromVal = from.val() || "",
				toVal = to.val() || "",
				ok = toVal >= fromVal;
			if (ok) return;

			if ($(this)[0] == from[0])
				to.val(from.val())
			else
				from.val(to.val());
		};
	from.change(compare);
	to.change(compare);
}

function ignore() {
	console.log.apply(console, arguments);
}
