(function (window, nodeGlobal) {
	var global = {
			jquery: (typeof(jQuery) != "undefined") ? jQuery : null
		},
		globalFunc = {},
		globalClass = {};

	// JUI의 기본 설정 값 (향후 더 추가될 수 있음)
	var globalOpts = {
		template: {
			evaluate: /<\!([\s\S]+?)\!>/g,
			interpolate: /<\!=([\s\S]+?)\!>/g,
			escape: /<\!-([\s\S]+?)\!>/g
		},
		logUrl: "tool/debug.html"
	};


	/**
	 * @class util.base
	 *
	 * jui 에서 공통적으로 사용하는 유틸리티 함수 모음
	 *
	 * ```
	 * var _ = jui.include("util.base");
	 *
	 * console.log(_.browser.webkit);
	 * ```
	 *
	 * @singleton
	 */
	var utility = global["util.base"] = {

		/**
		 * @property browser check browser agent
		 * @property {Boolean} browser.webkit  Webkit 브라우저 체크
		 * @property {Boolean} browser.mozilla  Mozilla 브라우저 체크
		 * @property {Boolean} browser.msie  IE 브라우저 체크 */
		browser: {
			webkit: ('WebkitAppearance' in document.documentElement.style) ? true : false,
			mozilla: (typeof window.mozInnerScreenX != "undefined") ? true : false,
			msie: (window.navigator.userAgent.indexOf("Trident") != -1) ? true : false
		},

		/**
		 * @property {Boolean} isTouch
		 * check touch device
		 */
		isTouch: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(window.navigator.userAgent),

		/**
		 * @method inherit
		 *
		 * 프로토타입 기반의 상속 제공
		 *
		 * @param {Function} ctor base Class
		 * @param {Function} superCtor super Class
		 */
		inherit: function (ctor, superCtor) {
			if (!this.typeCheck("function", ctor) || !this.typeCheck("function", superCtor)) return;

			ctor.parent = superCtor;
			ctor.prototype = new superCtor;
			ctor.prototype.constructor = ctor;
			ctor.prototype.parent = ctor.prototype;

			/**
			 * @method super
			 * call parent method
			 * @param {String} method  parent method name
			 * @param {Array} args
			 * @returns {Mixed}
			 */
			ctor.prototype.super = function (method, args) {
				return this.constructor.prototype[method].apply(this, args);
			}
		},

		/**
		 * @method extend
		 *
		 * implements object extend
		 *
		 * @param {Object|Function} origin
		 * @param {Object|Function} add
		 * @param {Boolean} skip
		 * @return {Object}
		 */
		extend: function (origin, add, skip) {
			if (!this.typeCheck([ "object", "function" ], origin)) origin = {};
			if (!this.typeCheck([ "object", "function" ], add)) return origin;

			for (var key in add) {
				if (skip === true) {
					if (isRecursive(origin[key])) {
						this.extend(origin[key], add[key], skip);
					} else if (this.typeCheck("undefined", origin[key])) {
						origin[key] = add[key];
					}
				} else {
					if (isRecursive(origin[key])) {
						this.extend(origin[key], add[key], skip);
					} else {
						origin[key] = add[key];
					}
				}
			}

			function isRecursive(value) {
				return utility.typeCheck("object", value);
			}

			return origin;
		},

		/**
		 * convert px to integer
		 * @param {String or Number} px
		 * @return {Number}
		 */
		pxToInt: function (px) {
			if (this.typeCheck("string", px) && px.indexOf("px") != -1) {
				return parseInt(px.split("px").join(""));
			}

			return px;
		},

		/**
		 * @method clone
		 * implements object clone
		 * @param {Array/Object} obj 복사할 객체
		 * @return {Array}
		 */
		clone: function (obj) {
			var clone = (this.typeCheck("array", obj)) ? [] : {};

			for (var i in obj) {
				if (this.typeCheck("object", obj[i]))
					clone[i] = this.clone(obj[i]);
				else
					clone[i] = obj[i];
			}

			return clone;
		},

		/**
		 * @method deepClone
		 * implements object deep clone
		 * @param obj
		 * @param emit
		 * @return {*}
		 */
		deepClone: function (obj, emit) {
			var value = null;
			emit = emit || {};

			if (this.typeCheck("array", obj)) {
				value = new Array(obj.length);

				for (var i = 0, len = obj.length; i < len; i++) {
					value[i] = this.deepClone(obj[i], emit);
				}
			} else if (this.typeCheck("date", obj)) {
				value = obj;
			} else if (this.typeCheck("object", obj)) {
				value = {};

				for (var key in obj) {
					if (emit[key]) {
						value[key] = obj[key];
					} else {
						value[key] = this.deepClone(obj[key], emit);
					}
				}
			} else {
				value = obj;
			}

			return value;
		},

		/**
		 * @method sort
		 * use QuickSort
		 * @param {Array} array
		 * @return {QuickSort}
		 */
		sort: function (array) {
			var QuickSort = jui.include("util.sort");
			return new QuickSort(array);
		},

		/**
		 * @method runtime
		 *
		 * caculate callback runtime
		 *
		 * @param {String} name
		 * @param {Function} callback
		 */
		runtime: function (name, callback) {
			var nStart = new Date().getTime();
			callback();
			var nEnd = new Date().getTime();

			console.log(name + " : " + (nEnd - nStart) + "ms");
		},

		/**
		 * @method template
		 * parsing template string
		 * @param html
		 * @param obj
		 */
		template: function (html, obj) {
			var tpl = jui.include("util.template");

			if (!obj) return tpl(html, null, globalOpts.template);
			else return tpl(html, obj, globalOpts.template);
		},

		/**
		 * @method resize
		 * add event in window resize event
		 * @param {Function} callback
		 * @param {Number} ms delay time
		 */
		resize: function (callback, ms) {
			var after_resize = (function () {
				var timer = 0;

				return function () {
					clearTimeout(timer);
					timer = setTimeout(callback, ms);
				}
			})();

			if (window.addEventListener) {
				window.addEventListener("resize", after_resize);
			} else if (object.attachEvent) {
				window.attachEvent("onresize", after_resize);
			} else {
				window["onresize"] = after_resize;
			}
		},

		/**
		 * @method index
		 *
		 * IndexParser 객체 생성
		 *
		 * @return {IndexParser}
		 */
		index: function () {
			var KeyParser = jui.include("util.keyparser");
			return new KeyParser();
		},

		/**
		 * @method chunk
		 * split array by length
		 * @param {Array} arr
		 * @param {Number} len
		 * @return {Array}
		 */
		chunk: function (arr, len) {
			var chunks = [],
				i = 0,
				n = arr.length;

			while (i < n) {
				chunks.push(arr.slice(i, i += len));
			}

			return chunks;
		},

		/**
		 * @method typeCheck
		 * check data  type
		 * @param {String} t  type string
		 * @param {Object} v value object
		 * @return {Boolean}
		 */
		typeCheck: function (t, v) {
			function check(type, value) {
				if (typeof(type) != "string") return false;

				if (type == "string") {
					return (typeof(value) == "string");
				}
				else if (type == "integer") {
					return (typeof(value) == "number" && value % 1 == 0);
				}
				else if (type == "float") {
					return (typeof(value) == "number" && value % 1 != 0);
				}
				else if (type == "number") {
					return (typeof(value) == "number");
				}
				else if (type == "boolean") {
					return (typeof(value) == "boolean");
				}
				else if (type == "undefined") {
					return (typeof(value) == "undefined");
				}
				else if (type == "null") {
					return (value === null);
				}
				else if (type == "array") {
					return (value instanceof Array);
				}
				else if (type == "date") {
					return (value instanceof Date);
				}
				else if (type == "function") {
					return (typeof(value) == "function");
				}
				else if (type == "object") {
					// typeCheck에 정의된 타입일 경우에는 object 체크시 false를 반환 (date, array, null)
					return (
					typeof(value) == "object" &&
					value !== null && !(value instanceof Array) && !(value instanceof Date) && !(value instanceof RegExp)
					);
				}

				return false;
			}

			if (typeof(t) == "object" && t.length) {
				var typeList = t;

				for (var i = 0; i < typeList.length; i++) {
					if (check(typeList[i], v)) return true;
				}

				return false;
			} else {
				return check(t, v);
			}
		},
		typeCheckObj: function (uiObj, list) {
			if (typeof(uiObj) != "object") return;
			var self = this;

			for (var key in uiObj) {
				var func = uiObj[key];

				if (typeof(func) == "function") {
					(function (funcName, funcObj) {
						uiObj[funcName] = function () {
							var args = arguments,
								params = list[funcName];

							for (var i = 0; i < args.length; i++) {
								if (!self.typeCheck(params[i], args[i])) {
									throw new Error("JUI_CRITICAL_ERR: the " + i + "th parameter is not a " + params[i] + " (" + name + ")");
								}
							}

							return funcObj.apply(this, args);
						}
					})(key, func);
				}
			}
		},

		/**
		 * @method dataToCsv
		 *
		 * data 를 csv 로 변환한다.
		 *
		 * @param {Array} keys
		 * @param {Array} dataList
		 * @param {Number} dataSize
		 * @return {String}  변환된 csv 문자열
		 */
		dataToCsv: function (keys, dataList, dataSize) {
			var csv = "", len = (!dataSize) ? dataList.length : dataSize;

			for (var i = -1; i < len; i++) {
				var tmpArr = [];

				for (var j = 0; j < keys.length; j++) {
					if (keys[j]) {
						if (i == -1) {
							tmpArr.push('"' + keys[j] + '"');
						} else {
							var value = dataList[i][keys[j]];
							tmpArr.push(isNaN(value) ? '"' + value + '"' : value);
						}
					}
				}

				csv += tmpArr.join(",") + "\n";
			}

			return csv;
		},

		/**
		 * @method dataToCsv2
		 *
		 * @param {Object} options
		 * @return {String}
		 */
		dataToCsv2: function (options) {
			var csv = "";
			var opts = this.extend({
				fields: null, // required
				rows: null, // required
				names: null,
				types: null,
				count: (this.typeCheck("integer", options.count)) ? options.count : options.rows.length
			}, options);

			for (var i = -1; i < opts.count; i++) {
				var tmpArr = [];

				for (var j = 0; j < opts.fields.length; j++) {
					if (opts.fields[j]) {
						if (i == -1) {
							if (opts.names && opts.names[j]) {
								tmpArr.push('"' + opts.names[j] + '"');
							} else {
								tmpArr.push('"' + opts.fields[j] + '"');
							}
						} else {
							var value = opts.rows[i][opts.fields[j]];

							if (this.typeCheck("array", opts.types)) {
								if(opts.types[j] == "string") {
									tmpArr.push('"' + value + '"');
								} else if(opts.types[j] == "integer") {
									tmpArr.push(parseInt(value));
								} else if(opts.types[j] == "float") {
									tmpArr.push(parseFloat(value));
								} else {
									tmpArr.push(value);
								}
							} else {
								tmpArr.push(isNaN(value) ? '"' + value + '"' : value);
							}
						}
					}
				}

				csv += tmpArr.join(",") + "\n";
			}

			return csv;
		},

		/**
		 * @method fileToCsv
		 *
		 * file 에서 csv 컨텐츠 로드
		 *
		 * @param {File} file
		 * @param {Function} callback
		 */
		fileToCsv: function (file, callback) {
			var reader = new FileReader();

			reader.onload = function (readerEvt) {
				if (utility.typeCheck("function", callback)) {
					callback(readerEvt.target.result);
				}
			};

			reader.readAsText(file);
		},
		/**
		 * @method csvToBase64
		 *
		 * csv 다운로드 링크로 변환
		 *
		 * @param {String} csv
		 * @return {String}
		 */
		csvToBase64: function (csv) {
			var Base64 = jui.include("util.base64");
			return "data:application/octet-stream;base64," + Base64.encode(csv);
		},
		/**
		 * @method csvToData
		 *
		 * @param {Array} keys
		 * @param {String} csv
		 * @param {Number} csvNumber
		 * @return {Array}
		 */
		csvToData: function (keys, csv, csvNumber) {
			var dataList = [],
				tmpRowArr = csv.split("\n")

			for (var i = 1; i < tmpRowArr.length; i++) {
				if (tmpRowArr[i] != "") {
					var tmpArr = tmpRowArr[i].split(","), // TODO: 값 안에 콤마(,)가 있을 경우에 별도로 처리해야 함
						data = {};

					for (var j = 0; j < keys.length; j++) {
						data[keys[j]] = tmpArr[j];

						// '"' 로 감싸져있는 문자열은 '"' 제거
						if (this.startsWith(tmpArr[j], '"') && this.endsWith(tmpArr[j], '"')) {
							data[keys[j]] = tmpArr[j].split('"').join('');
						} else {
							data[keys[j]] = tmpArr[j];
						}

						if (this.inArray(keys[j], csvNumber) != -1) {
							data[keys[j]] = parseFloat(tmpArr[j]);
						}
					}

					dataList.push(data);
				}
			}

			return dataList;
		},
		/**
		 * @method getCsvFields
		 *
		 * csv 에서 필드 얻어오기
		 *
		 * @param {Array} fields
		 * @param {Array} csvFields
		 * @return {Array}
		 */
		getCsvFields: function (fields, csvFields) {
			var tmpFields = (this.typeCheck("array", csvFields)) ? csvFields : fields;

			for (var i = 0; i < tmpFields.length; i++) {
				if (!isNaN(tmpFields[i])) {
					tmpFields[i] = fields[tmpFields[i]];
				}
			}

			return tmpFields;
		},

		/**
		 * @method svgToBase64
		 *
		 * xml 문자열로 svg datauri 생성
		 *
		 * @param {String} xml
		 * @return {String} 변환된 data uri 링크
		 */
		svgToBase64: function (xml) {
			var Base64 = jui.include("util.base64");
			return "data:image/svg+xml;base64," + Base64.encode(xml);
		},

		/**
		 * @method dateFormat
		 *
		 * implements date format function
		 *
		 * yyyy : 4 digits year
		 * yy : 2 digits year
		 * y : 1 digit year
		 *
		 * @param {Date} date
		 * @param {String} format   date format string
		 * @param utc
		 * @return {string}
		 */
		dateFormat: function (date, format, utc) {
			var MMMM = ["\x00", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
			var MMM = ["\x01", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
			var dddd = ["\x02", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
			var ddd = ["\x03", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];

			function ii(i, len) {
				var s = i + "";
				len = len || 2;
				while (s.length < len) s = "0" + s;
				return s;
			}

			var y = utc ? date.getUTCFullYear() : date.getFullYear();
			format = format.replace(/(^|[^\\])yyyy+/g, "$1" + y);
			format = format.replace(/(^|[^\\])yy/g, "$1" + y.toString().substr(2, 2));
			format = format.replace(/(^|[^\\])y/g, "$1" + y);

			var M = (utc ? date.getUTCMonth() : date.getMonth()) + 1;
			format = format.replace(/(^|[^\\])MMMM+/g, "$1" + MMMM[0]);
			format = format.replace(/(^|[^\\])MMM/g, "$1" + MMM[0]);
			format = format.replace(/(^|[^\\])MM/g, "$1" + ii(M));
			format = format.replace(/(^|[^\\])M/g, "$1" + M);

			var d = utc ? date.getUTCDate() : date.getDate();
			format = format.replace(/(^|[^\\])dddd+/g, "$1" + dddd[0]);
			format = format.replace(/(^|[^\\])ddd/g, "$1" + ddd[0]);
			format = format.replace(/(^|[^\\])dd/g, "$1" + ii(d));
			format = format.replace(/(^|[^\\])d/g, "$1" + d);

			var H = utc ? date.getUTCHours() : date.getHours();
			format = format.replace(/(^|[^\\])HH+/g, "$1" + ii(H));
			format = format.replace(/(^|[^\\])H/g, "$1" + H);

			var h = H > 12 ? H - 12 : H == 0 ? 12 : H;
			format = format.replace(/(^|[^\\])hh+/g, "$1" + ii(h));
			format = format.replace(/(^|[^\\])h/g, "$1" + h);

			var m = utc ? date.getUTCMinutes() : date.getMinutes();
			format = format.replace(/(^|[^\\])mm+/g, "$1" + ii(m));
			format = format.replace(/(^|[^\\])m/g, "$1" + m);

			var s = utc ? date.getUTCSeconds() : date.getSeconds();
			format = format.replace(/(^|[^\\])ss+/g, "$1" + ii(s));
			format = format.replace(/(^|[^\\])s/g, "$1" + s);

			var f = utc ? date.getUTCMilliseconds() : date.getMilliseconds();
			format = format.replace(/(^|[^\\])fff+/g, "$1" + ii(f, 3));
			f = Math.round(f / 10);
			format = format.replace(/(^|[^\\])ff/g, "$1" + ii(f));
			f = Math.round(f / 10);
			format = format.replace(/(^|[^\\])f/g, "$1" + f);

			var T = H < 12 ? "AM" : "PM";
			format = format.replace(/(^|[^\\])TT+/g, "$1" + T);
			format = format.replace(/(^|[^\\])T/g, "$1" + T.charAt(0));

			var t = T.toLowerCase();
			format = format.replace(/(^|[^\\])tt+/g, "$1" + t);
			format = format.replace(/(^|[^\\])t/g, "$1" + t.charAt(0));

			var tz = -date.getTimezoneOffset();
			var K = utc || !tz ? "Z" : tz > 0 ? "+" : "-";
			if (!utc) {
				tz = Math.abs(tz);
				var tzHrs = Math.floor(tz / 60);
				var tzMin = tz % 60;
				K += ii(tzHrs) + ":" + ii(tzMin);
			}
			format = format.replace(/(^|[^\\])K/g, "$1" + K);

			var day = (utc ? date.getUTCDay() : date.getDay()) + 1;
			format = format.replace(new RegExp(dddd[0], "g"), dddd[day]);
			format = format.replace(new RegExp(ddd[0], "g"), ddd[day]);

			format = format.replace(new RegExp(MMMM[0], "g"), MMMM[M]);
			format = format.replace(new RegExp(MMM[0], "g"), MMM[M]);

			format = format.replace(/\\(.)/g, "$1");

			return format;
		},
		/**
		 * @method createId
		 *
		 * 유니크 아이디 생성
		 *
		 * @param {String} key  prefix string
		 * @return {String} 생성된 아이디 문자열
		 */
		createId: function (key) {
			return [key || "id", (+new Date), Math.round(Math.random() * 100) % 100].join("-");
		},
		/**
		 * @method btoa
		 *
		 * Base64 인코딩
		 *
		 * @return {String}
		 */
		btoa: function(input) {
			var Base64 = jui.include("util.base64");
			return Base64.encode(input);
		},
		/**
		 * @method atob
		 *
		 * Base64 디코딩
		 *
		 * @return {String}
		 */
		atob: function(input) {
			var Base64 = jui.include("util.base64");
			return Base64.decode(input);
		},

		/**
		 * implement async loop without blocking ui
		 *
		 * @param total
		 * @param context
		 * @returns {Function}
		 */
		timeLoop : function(total, context) {

			return function(callback, lastCallback) {
				function TimeLoopCallback (i) {

					if (i < 1) return;

					if (i == 1) {
						callback.call(context, i)
						lastCallback.call(context);
					} else {
						setTimeout(function() {
							if (i > -1) callback.call(context, i--);
							if (i > -1) TimeLoopCallback(i);
						}, 1);
					}
				}

				TimeLoopCallback(total);
			};
		},
		/**
		 * @method loop
		 *
		 * 최적화된 루프 생성 (5단계로 나눔)
		 *
		 * @param {Number} total
		 * @param {Object} [context=null]
		 * @return {Function} 최적화된 루프 콜백 (index, groupIndex 2가지 파라미터를 받는다.)
		 */
		loop: function (total, context) {
			var start = 0,
				end = total,
				unit = Math.ceil(total / 5);

			return function (callback) {
				var first = start, second = unit * 1, third = unit * 2, fourth = unit * 3, fifth = unit * 4,
					firstMax = second, secondMax = third, thirdMax = fourth, fourthMax = fifth, fifthMax = end;

				while (first < firstMax && first < end) {
					callback.call(context, first, 1);
					first++;

					if (second < secondMax && second < end) {
						callback.call(context, second, 2);
						second++;
					}
					if (third < thirdMax && third < end) {
						callback.call(context, third, 3);
						third++;
					}
					if (fourth < fourthMax && fourth < end) {
						callback.call(context, fourth, 4);
						fourth++;
					}
					if (fifth < fifthMax && fifth < end) {
						callback.call(context, fifth, 5);
						fifth++;
					}
				}
			};
		},

		/**
		 * @method loopArray
		 *
		 * 배열을 사용해서 최적화된 루프로 생성한다.
		 *
		 *
		 * @param {Array} data 루프로 생성될 배열
		 * @param {Object} [context=null]
		 * @return {Function} 최적화된 루프 콜백 (data, index, groupIndex 3가지 파라미터를 받는다.)
		 */
		loopArray: function (data, context) {
			var total = data.length,
				start = 0,
				end = total,
				unit = Math.ceil(total / 5);

			return function (callback) {
				var first = start, second = unit * 1, third = unit * 2, fourth = unit * 3, fifth = unit * 4,
					firstMax = second, secondMax = third, thirdMax = fourth, fourthMax = fifth, fifthMax = end;

				while (first < firstMax && first < end) {
					callback.call(context, data[first], first, 1);
					first++;
					if (second < secondMax && second < end) {
						callback.call(context, data[second], second, 2);
						second++;
					}
					if (third < thirdMax && third < end) {
						callback.call(context, data[third], third, 3);
						third++;
					}
					if (fourth < fourthMax && fourth < end) {
						callback.call(context, data[fourth], fourth, 4);
						fourth++;
					}
					if (fifth < fifthMax && fifth < end) {
						callback.call(context, data[fifth], fifth, 5);
						fifth++;
					}
				}
			};

		},

		/**
		 * @method makeIndex
		 *
		 * 배열의 키 기반 인덱스를 생성한다.
		 *
		 * 개별 값 별로 멀티 인덱스를 생성한다.
		 *
		 * @param {Array} data
		 * @param {String} keyField
		 * @return {Object} 생성된 인덱스
		 */
		makeIndex: function (data, keyField) {
			var list = {},
				func = this.loopArray(data);

			func(function (d, i) {
				var value = d[keyField];

				if (typeof list[value] == 'undefined') {
					list[value] = [];
				}

				list[value].push(i);
			});

			return list;
		},

		/**
		 * @method startsWith
		 * Check that it matches the starting string search string.
		 *
		 * @param {String} string
		 * @param {String} searchString
		 * @return {Integer} position
		 */
		startsWith: function (string, searchString, position) {
			position = position || 0;

			return string.lastIndexOf(searchString, position) === position;
		},

		/**
		 * @method endsWith
		 * Check that it matches the end of a string search string.
		 *
		 * @param {String} string
		 * @param {String} searchString
		 * @return {Integer} position
		 */
		endsWith: function (string, searchString, position) {
			var subjectString = string;

			if (position === undefined || position > subjectString.length) {
				position = subjectString.length;
			}

			position -= searchString.length;
			var lastIndex = subjectString.indexOf(searchString, position);

			return lastIndex !== -1 && lastIndex === position;
		},

		inArray: function (target, list) {
			if(this.typeCheck([ "undefined", "null" ], target) ||
				!this.typeCheck("array", list)) return -1;

			for(var i = 0, len = list.length; i < len; i++) {
				if(list[i] == target)
					return i;
			}

			return -1;
		},

		trim: function(text) {
			var whitespace = "[\\x20\\t\\r\\n\\f]",
				rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" );

			return text == null ?
				"" :
				( text + "" ).replace( rtrim, "" );
		},

		ready: (function() {
			var readyList,
				DOMContentLoaded,
				class2type = {};

			class2type["[object Boolean]"] = "boolean";
			class2type["[object Number]"] = "number";
			class2type["[object String]"] = "string";
			class2type["[object Function]"] = "function";
			class2type["[object Array]"] = "array";
			class2type["[object Date]"] = "date";
			class2type["[object RegExp]"] = "regexp";
			class2type["[object Object]"] = "object";

			var ReadyObj = {
				// Is the DOM ready to be used? Set to true once it occurs.
				isReady: false,
				// A counter to track how many items to wait for before
				// the ready event fires. See #6781
				readyWait: 1,
				// Hold (or release) the ready event
				holdReady: function( hold ) {
					if ( hold ) {
						ReadyObj.readyWait++;
					} else {
						ReadyObj.ready( true );
					}
				},
				// Handle when the DOM is ready
				ready: function( wait ) {
					// Either a released hold or an DOMready/load event and not yet ready
					if ( (wait === true && !--ReadyObj.readyWait) || (wait !== true && !ReadyObj.isReady) ) {
						// Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
						if ( !document.body ) {
							return setTimeout( ReadyObj.ready, 1 );
						}

						// Remember that the DOM is ready
						ReadyObj.isReady = true;
						// If a normal DOM Ready event fired, decrement, and wait if need be
						if ( wait !== true && --ReadyObj.readyWait > 0 ) {
							return;
						}
						// If there are functions bound, to execute
						readyList.resolveWith( document, [ ReadyObj ] );

						// Trigger any bound ready events
						//if ( ReadyObj.fn.trigger ) {
						//  ReadyObj( document ).trigger( "ready" ).unbind( "ready" );
						//}
					}
				},
				bindReady: function() {
					if ( readyList ) {
						return;
					}
					readyList = ReadyObj._Deferred();

					// Catch cases where $(document).ready() is called after the
					// browser event has already occurred.
					if ( document.readyState === "complete" ) {
						// Handle it asynchronously to allow scripts the opportunity to delay ready
						return setTimeout( ReadyObj.ready, 1 );
					}

					// Mozilla, Opera and webkit nightlies currently support this event
					if ( document.addEventListener ) {
						// Use the handy event callback
						document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
						// A fallback to window.onload, that will always work
						window.addEventListener( "load", ReadyObj.ready, false );

						// If IE event model is used
					} else if ( document.attachEvent ) {
						// ensure firing before onload,
						// maybe late but safe also for iframes
						document.attachEvent( "onreadystatechange", DOMContentLoaded );

						// A fallback to window.onload, that will always work
						window.attachEvent( "onload", ReadyObj.ready );

						// If IE and not a frame
						// continually check to see if the document is ready
						var toplevel = false;

						try {
							toplevel = window.frameElement == null;
						} catch(e) {}

						if ( document.documentElement.doScroll && toplevel ) {
							doScrollCheck();
						}
					}
				},
				_Deferred: function() {
					var // callbacks list
						callbacks = [],
					// stored [ context , args ]
						fired,
					// to avoid firing when already doing so
						firing,
					// flag to know if the deferred has been cancelled
						cancelled,
					// the deferred itself
						deferred  = {

							// done( f1, f2, ...)
							done: function() {
								if ( !cancelled ) {
									var args = arguments,
										i,
										length,
										elem,
										type,
										_fired;
									if ( fired ) {
										_fired = fired;
										fired = 0;
									}
									for ( i = 0, length = args.length; i < length; i++ ) {
										elem = args[ i ];
										type = ReadyObj.type( elem );
										if ( type === "array" ) {
											deferred.done.apply( deferred, elem );
										} else if ( type === "function" ) {
											callbacks.push( elem );
										}
									}
									if ( _fired ) {
										deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] );
									}
								}
								return this;
							},

							// resolve with given context and args
							resolveWith: function( context, args ) {
								if ( !cancelled && !fired && !firing ) {
									// make sure args are available (#8421)
									args = args || [];
									firing = 1;
									try {
										while( callbacks[ 0 ] ) {
											callbacks.shift().apply( context, args );//shifts a callback, and applies it to document
										}
									}
									finally {
										fired = [ context, args ];
										firing = 0;
									}
								}
								return this;
							},

							// resolve with this as context and given arguments
							resolve: function() {
								deferred.resolveWith( this, arguments );
								return this;
							},

							// Has this deferred been resolved?
							isResolved: function() {
								return !!( firing || fired );
							},

							// Cancel
							cancel: function() {
								cancelled = 1;
								callbacks = [];
								return this;
							}
						};

					return deferred;
				},
				type: function( obj ) {
					return obj == null ?
						String( obj ) :
					class2type[ Object.prototype.toString.call(obj) ] || "object";
				}
			}
			// The DOM ready check for Internet Explorer
			function doScrollCheck() {
				if ( ReadyObj.isReady ) {
					return;
				}

				try {
					// If IE is used, use the trick by Diego Perini
					// http://javascript.nwbox.com/IEContentLoaded/
					document.documentElement.doScroll("left");
				} catch(e) {
					setTimeout( doScrollCheck, 1 );
					return;
				}

				// and execute any waiting functions
				ReadyObj.ready();
			}
			// Cleanup functions for the document ready method
			if ( document.addEventListener ) {
				DOMContentLoaded = function() {
					document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
					ReadyObj.ready();
				};

			} else if ( document.attachEvent ) {
				DOMContentLoaded = function() {
					// Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443).
					if ( document.readyState === "complete" ) {
						document.detachEvent( "onreadystatechange", DOMContentLoaded );
						ReadyObj.ready();
					}
				};
			}
			function ready( fn ) {
				// Attach the listeners
				ReadyObj.bindReady();

				var type = ReadyObj.type( fn );

				// Add the callback
				readyList.done( fn );//readyList is result of _Deferred()
			}

			return ready;
		})(),

		param: function(data) {
			var r20 = /%20/g,
				s = [],
				add = function(key, value) {
					// If value is a function, invoke it and return its value
					value = utility.typeCheck("function", value) ? value() : (value == null ? "" : value);
					s[s.length] = encodeURIComponent(key) + "=" + encodeURIComponent(value);
				};

			for(var key in data) {
				add(key, data[key]);
			}

			return s.join("&").replace(r20, "+");
		},

		ajax: function(data) {
			var xhr = null, paramStr = "", callback = null;

			var opts = utility.extend({
				url: null,
				type: "GET",
				data: null,
				async: true,
				success: null,
				fail: null
			}, data);

			if(!this.typeCheck("string", opts.url) || !this.typeCheck("function", opts.success))
				return;

			if(this.typeCheck("object", opts.data))
				paramStr = this.param(opts.data);

			if(!this.typeCheck("undefined", XMLHttpRequest)) {
				xhr = new XMLHttpRequest();
			} else {
				var versions = [
					"MSXML2.XmlHttp.5.0",
					"MSXML2.XmlHttp.4.0",
					"MSXML2.XmlHttp.3.0",
					"MSXML2.XmlHttp.2.0",
					"Microsoft.XmlHttp"
				];

				for(var i = 0, len = versions.length; i < len; i++) {
					try {
						xhr = new ActiveXObject(versions[i]);
						break;
					}
					catch(e) {}
				}
			}

			if(xhr != null) {
				xhr.open(opts.type, opts.url, opts.async);
				xhr.send(paramStr);

				callback = function() {
					if (xhr.readyState === 4 && xhr.status == 200) {
						opts.success(xhr);
					} else {
						if (utility.typeCheck("function", opts.fail)) {
							opts.fail(xhr);
						}
					}
				}

				if (!opts.async) {
					callback();
				} else {
					xhr.onreadystatechange = callback;
				}
			}
		},

		scrollWidth: function() {
			var inner = document.createElement("p");
			inner.style.width = "100%";
			inner.style.height = "200px";

			var outer = document.createElement("div");
			outer.style.position = "absolute";
			outer.style.top = "0px";
			outer.style.left = "0px";
			outer.style.visibility = "hidden";
			outer.style.width = "200px";
			outer.style.height = "150px";
			outer.style.overflow = "hidden";
			outer.appendChild(inner);

			document.body.appendChild(outer);
			var w1 = inner.offsetWidth;
			outer.style.overflow = "scroll";
			var w2 = inner.offsetWidth;
			if (w1 == w2) w2 = outer.clientWidth;
			document.body.removeChild(outer);

			return (w1 - w2);
		}
	}


	/*
	 * Module related functions
	 *
	 */
	var getDepends = function (depends) {
		var args = [];

		for (var i = 0; i < depends.length; i++) {
			var module = global[depends[i]];

			if (!utility.typeCheck([ "function", "object" ], module)) {
				var modules = getModules(depends[i]);

				if (modules == null) {
					console.log("JUI_WARNING_MSG: '" + depends[i] + "' is not loaded");
					args.push(null);
				} else {
					args.push(modules);
				}

			} else {
				args.push(module);
			}
		}

		return args;
	}

	var getModules = function (parent) {
		var modules = null,
			parent = parent + ".";

		for (var key in global) {
			if (key.indexOf(parent) != -1) {
				if (utility.typeCheck([ "function", "object" ], global[key])) {
					var child = key.split(parent).join("");

					if (child.indexOf(".") == -1) {
						if (modules == null) {
							modules = {};
						}

						modules[child] = global[key];
					}
				}
			}
		}

		return modules;
	}

	/**
	 * @class jui
	 *
	 * Global Object
	 *
	 * @singleton
	 */
	window.jui = nodeGlobal.jui = {

		/**
		 * @method ready
		 *
		 * ready 타임에 실행될 callback 정의
		 *
		 * @param {Function} callback
		 */
		ready: function () {
			var args = [],
				callback = (arguments.length == 2) ? arguments[1] : arguments[0],
				depends = (arguments.length == 2) ? arguments[0] : null;

			if (!utility.typeCheck([ "array", "null" ], depends) || !utility.typeCheck("function", callback)) {
				throw new Error("JUI_CRITICAL_ERR: Invalid parameter type of the function");
			}

			utility.ready(function() {
				if (depends) {
					args = getDepends(depends);
				} else {
					// @Deprecated 기존의 레거시를 위한 코드
					var ui = getModules("ui"),
						uix = {};

					utility.extend(uix, ui);
					utility.extend(uix, getModules("grid"));

					args = [ ui, uix, utility ];
				}

				callback.apply(null, args);
			});
		},

		/**
		 * @method defineUI
		 *
		 * 사용자가 실제로 사용할 수 있는 UI 클래스를 정의
		 *
		 * @param {String} name 모듈 로드와 상속에 사용될 이름을 정한다.
		 * @param {Array} depends 'define'이나 'defineUI'로 정의된 클래스나 객체를 인자로 받을 수 있다.
		 * @param {Function} callback UI 클래스를 해당 콜백 함수 내에서 클래스 형태로 구현하고 리턴해야 한다.
		 */
		defineUI: function (name, depends, callback, parent) {
			if (!utility.typeCheck("string", name) || !utility.typeCheck("array", depends) ||
				!utility.typeCheck("function", callback) || !utility.typeCheck([ "string", "undefined" ], parent)) {
				throw new Error("JUI_CRITICAL_ERR: Invalid parameter type of the function");
			}

			if (utility.typeCheck("function", globalClass[name])) {
				throw new Error("JUI_CRITICAL_ERR: '" + name + "' is already exist");
			}

			if (utility.typeCheck("undefined", parent)) { // 기본적으로 'event' 클래스를 상속함
				parent = "event";
			}

			if (!utility.typeCheck("function", globalClass[parent])) {
				throw new Error("JUI_CRITICAL_ERR: Parents are the only function");
			} else {
				if (globalFunc[parent] !== true) {
					throw new Error("JUI_CRITICAL_ERR: UI function can not be inherited");
				}
			}

			var args = getDepends(depends),
				uiFunc = callback.apply(null, args);

			// 상속
			utility.inherit(uiFunc, globalClass[parent]);

			// TODO: 차트 빌더를 제외하고, 무조건 event 클래스에 정의된 init 메소드를 호출함
			global[name] = globalClass[parent != "core" ? "event" : "core"].init({
				type: name,
				"class": uiFunc
			});

			globalClass[name] = uiFunc;
			globalFunc[name] = true;

			/**
			 * @deprecated
				// support AMD module
			if (typeof define == "function" && define.amd) {
				define(name, function () {
					return global[name]
				});
			}
			 */
		},

		createUIObject: function (UI, selector, index, elem, options, afterHook) {
			var mainObj = new UI["class"]();

			// Check Options
			var opts = jui.defineOptions(UI["class"], options || {});

			// Public Properties
			mainObj.init.prototype = mainObj;
			/** @property {String/HTMLElement} selector */
			mainObj.init.prototype.selector = selector;
			/** @property {HTMLElement} root */
			mainObj.init.prototype.root = elem;
			/** @property {Object} options */
			mainObj.init.prototype.options = opts;
			/** @property {Object} tpl Templates */
			mainObj.init.prototype.tpl = {};
			/** @property {Array} event Custom events */
			mainObj.init.prototype.event = new Array(); // Custom Event
			/** @property {Integer} timestamp UI Instance creation time*/
			mainObj.init.prototype.timestamp = new Date().getTime();
			/** @property {Integer} index Index of UI instance*/
			mainObj.init.prototype.index = index;
			/** @property {Class} module Module class */
			mainObj.init.prototype.module = UI;

			// UI 객체 프로퍼티를 외부에서 정의할 수 있음 (jQuery 의존성 제거를 위한 코드)
			if(utility.typeCheck("function", afterHook)) {
				afterHook(mainObj, opts);
			}

			// Script-based Template Settings
			for (var name in opts.tpl) {
				var tplHtml = opts.tpl[name];

				if (utility.typeCheck("string", tplHtml) && tplHtml != "") {
					mainObj.init.prototype.tpl[name] = utility.template(tplHtml);
				}
			}

			var uiObj = new mainObj.init();

			// Custom Event Setting
			for(var key in opts.event) {
				uiObj.on(key, opts.event[key]);
			}

			// 엘리먼트 객체에 jui 속성 추가
			elem.jui = uiObj;

			return uiObj;
		},

		/**
		 * @method define
		 *
		 * UI 클래스에서 사용될 클래스를 정의하고, 자유롭게 상속할 수 있는 클래스를 정의
		 *
		 * @param {String} name 모듈 로드와 상속에 사용될 이름을 정한다.
		 * @param {Array} depends 'define'이나 'defineUI'로 정의된 클래스나 객체를 인자로 받을 수 있다.
		 * @param {Function} callback UI 클래스를 해당 콜백 함수 내에서 클래스 형태로 구현하고 리턴해야 한다.
		 * @param {String} parent 상속받을 클래스
		 */
		define: function (name, depends, callback, parent) {
			if (!utility.typeCheck("string", name) || !utility.typeCheck("array", depends) ||
				!utility.typeCheck("function", callback) || !utility.typeCheck([ "string", "undefined", "null" ], parent)) {
				throw new Error("JUI_CRITICAL_ERR: Invalid parameter type of the function");
			}

			if (utility.typeCheck("function", globalClass[name])) {
				throw new Error("JUI_CRITICAL_ERR: '" + name + "' is already exist");
			}

			var args = getDepends(depends),
				uiFunc = callback.apply(null, args);

			if (utility.typeCheck("function", globalClass[parent])) {
				if (globalFunc[parent] !== true) {
					throw new Error("JUI_CRITICAL_ERR: UI function can not be inherited");
				} else {
					utility.inherit(uiFunc, globalClass[parent]);
				}
			}

			// 함수 고유 설정
			global[name] = uiFunc;
			globalClass[name] = uiFunc; // original function
			globalFunc[name] = true;

			// support AMD module
			// @deprecated
			/*
			if (typeof define == "function" && define.amd) {
				define(name, function () {
					return global[name]
				});
			}*/
		},

		/**
		 * @method redefine
		 *
		 * UI 클래스에서 사용될 클래스를 정의하고, 자유롭게 상속할 수 있는 클래스를 정의
		 *
		 * @param {String} name 모듈 로드와 상속에 사용될 이름을 정한다.
		 * @param {Array} depends 'define'이나 'defineUI'로 정의된 클래스나 객체를 인자로 받을 수 있다.
		 * @param {Function} callback UI 클래스를 해당 콜백 함수 내에서 클래스 형태로 구현하고 리턴해야 한다.
		 * @param {String} parent 상속받을 클래스
		 */
		redefine: function (name, depends, callback, parent, skip) {
            if (!skip && globalFunc[name] === true) {
                global[name] = null;
                globalClass[name] = null;
                globalFunc[name] = false;
            }

            if (!skip || (skip && globalFunc[name] !== true)) {
                this.define(name, depends, callback, parent);
            }
		},

		/**
		 * @method defineOptions
		 *
		 * 모듈 기본 옵션 정의
		 *
		 * @param {Object} Module
		 * @param {Object} options
		 * @param {Object} exceptOpts
		 * @return {Object}
		 */
		defineOptions: function (Module, options, exceptOpts) {
			var defOpts = getOptions(Module, {});
			var defOptKeys = Object.keys(defOpts),
				optKeys = Object.keys(options);

			// 정의되지 않은 옵션 사용 유무 체크
			for (var i = 0; i < optKeys.length; i++) {
				var name = optKeys[i];

				if (utility.inArray(name, defOptKeys) == -1 && utility.inArray(name, exceptOpts) == -1) {
					throw new Error("JUI_CRITICAL_ERR: '" + name + "' is not an option");
				}
			}

			// 사용자 옵션 + 기본 옵션
			utility.extend(options, defOpts, true);

			// 상위 모듈의 옵션까지 모두 얻어오는 함수
			function getOptions(Module, options) {
				if (utility.typeCheck("function", Module)) {
					if (utility.typeCheck("function", Module.setup)) {
						var opts = Module.setup();

						for (var key in opts) {
							if (utility.typeCheck("undefined", options[key])) {
								options[key] = opts[key];
							}
						}
					}

					getOptions(Module.parent, options);
				}

				return options;
			}

			return options;
		},

		/**
		 * define과 defineUI로 정의된 클래스 또는 객체를 가져온다.
		 *
		 * @param name 가져온 클래스 또는 객체의 이름
		 * @return {*}
		 */
		include: function (name) {
			if (!utility.typeCheck("string", name)) {
				throw new Error("JUI_CRITICAL_ERR: Invalid parameter type of the function");
			}

			var module = global[name];

			if (utility.typeCheck(["function", "object"], module)) {
				return module;
			} else {
				var modules = getModules(name);

				if (modules == null) {
					console.log("JUI_WARNING_MSG: '" + name + "' is not loaded");
					return null;
				} else {
					return modules;
				}
			}
		},

		/**
		 * define과 defineUI로 정의된 모든 클래스와 객체를 가져온다.
		 *
		 * @return {Array}
		 */
		includeAll: function () {
			var result = [];

			for (var key in global) {
				result.push(global[key]);
			}

			return result;
		},

		/**
		 * 설정된 jui 관리 화면을 윈도우 팝업으로 띄운다.
		 *
		 * @param logUrl
		 * @return {Window}
		 */
		log: function (logUrl) {
			var jui_mng = window.open(
				logUrl || globalOpts.logUrl,
				"JUIM",
				"width=1024, height=768, toolbar=no, menubar=no, resizable=yes"
			);

			jui.debugAll(function (log, str) {
				jui_mng.log(log, str);
			});

			return jui_mng;
		},

		setup: function (options) {
			if (utility.typeCheck("object", options)) {
				globalOpts = utility.extend(globalOpts, options);
			}

			return globalOpts;
		}
	};

	if (typeof module == 'object' && module.exports) {
		module.exports = window.jui || global.jui;
	}

})(window, (typeof(global) !== "undefined") ? global : window);