jui.defineUI("grid.xtable", [ "jquery", "util.base", "ui.modal", "grid.table", "grid.row" ], function($, _, modal, table, Row) {

	_.resize(function() {
		var call_list = jui.get("grid.xtable");

		for(var i = 0; i < call_list.length; i++) {
			var ui_list = call_list[i];

			for(var j = 0; j < ui_list.length; j++) {
				ui_list[j].resize();
			}
		}
	}, 1000);

    /**
     * @class grid.xtable
     * @extends core
     * @alias X-Table
     * @requires util.base
     * @requires ui.modal
     * @requires grid.table
     *
     */
	var UI = function() {
		var head = null, body = null;
		var rows = [], c_rows = [],	t_rows = [], o_rows = null; // 루트 rows, 루트 rows 인덱스, 자식 포함 rows, 자식 제외 + 필터 rows (리펙토링 필요함!!!)
		var ui_modal = null, page = 1;
        var is_loading = false, is_resize = false;
		var w_resize = 8, select_row = null;
		var iParser = _.index();
		var vscroll_info = null;
		var xss_filter_keys = null;

		function createRows(data, no, pRow, type) {
			var tmp_rows = [];

			for(var i = 0, len = data.length; i < len; i++) {
				var row = new Row(),
					rownum = no + i;

				// row 객체 초기화
				row.init(data[i], head.tpl["row"], pRow);
				row.setIndex(rownum);

				// row 상태 설정
				if(type == "open" || type == "fold") {
					row.type = type;
				}

				// 루트 row만 캐싱함
				if(pRow == null) {
					c_rows[rownum] = row;
				}

				tmp_rows.push(row);
			}

			return tmp_rows;
		}

		function createTableList(self) {
			var exceptOpts = [
			   "buffer", "bufferCount", "csvCount", "sortLoading", "sortCache", "sortIndex", "sortOrder",
			   "event", "rows", "scrollWidth", "width", "rowHeight", "xssFilter", "msort"
			];

			var $root = $(self.root);

			// 가상스크롤 모드일 때, 로우 높이는 고정되야 하므로 nowrap 클래스를 추가함
			if(self.options.buffer == "vscroll") {
				if(!$root.hasClass("nowrap")) {
					$root.addClass("nowrap");
				}
			}

			// 스크롤 모드일 때, 무조건 scroll 클래스를 추가함
            if(self.options.buffer != "page") {
                if(!$root.hasClass("scroll")) {
                    $root.addClass("scroll");
                }
            }

			// 기본 테이블 마크업 복사해서 추가하기
			$root.append($root.children("table").clone());

			head = table($root.children("table:first-child"), getExceptOptions(self, exceptOpts)); // 헤더 테이블 생성
			setTableHeadStyle(self, head);

			body = table($root.children("table:last-child"), getExceptOptions(self, exceptOpts.concat("resize"))); // 바디 테이블 생성
			setTableBodyStyle(self, body); // X-Table 생성 및 마크업 설정

			// 공통 테이블 스타일 정의
			setTableAllStyle(self, head, body);

            // TODO: XSS 필터 대상 컬럼 설정 리펙토링 필요
            if(self.options.xssFilter) {
                var filterIndexes = self.options.xssFilter,
                    len = (filterIndexes === true) ? head.uit.getColumnCount() : filterIndexes.length;

                xss_filter_keys = {};
                for(var i = 0; i < len; i++) {
                    var colKey = (filterIndexes === true) ? i : filterIndexes[i],
                        col = head.getColumn(colKey);

                    xss_filter_keys[col.name] = true;
                }
            }

            // 테이블 옵션 필터링 함수
			function getExceptOptions(self, exceptOpts) {
				var options = {};

				for(var key in self.options) {
					if($.inArray(key, exceptOpts) == -1) {
						options[key] = self.options[key];
					}
				}

				// 가로 스크롤 모드일 때, resize 옵션 막기
				if(self.options.scrollWidth > 0) {
					options.resize = false;
				}

				return options;
			}

			function setTableAllStyle(self, head, body) {
				var opts = self.options;

				if(opts.scrollWidth > 0) {
					self.scrollWidth(opts.scrollWidth, true);
				} else {
					if(opts.width > 0) {
						$(self.root).outerWidth(opts.width);
					}
				}
			}

			function setTableHeadStyle(self, head) {
				$(head.root).wrap("<div class='head'></div>");
				$(head.root).children("tbody").remove();
			}

			function setTableBodyStyle(self, body) {
				var cols = body.listColumn();

				// X-Table 바디 영역 스크롤 높이 설정
				if (self.options.buffer != "page") {
					var scrollHeight = self.options.scrollHeight;

					if (self.options.buffer == "vscroll") {
						$(body.root).wrap("<div class='body' style='max-height: " + scrollHeight + "px'><div></div></div>");

						$(body.root).parent().parent().css({
							"overflow-y": "scroll"
						});
					} else {
						$(body.root).wrap("<div class='body' style='max-height: " + scrollHeight + "px'></div>");

						$(body.root).parent().css({
							"overflow-y": "scroll"
						});
					}
				} else {
					$(body.root).wrap("<div class='body'></div>");
				}

                // X-Table 바디 영역의 헤더라인은 마지막 노드를 제외하고 제거
                $(body.root).find("thead > tr").outerHeight(0).not(":last-child").remove();

				// X-Table 바디 영역의 헤더 설정
				for(var i = 0; i < cols.length; i++) {
					$(cols[i].element).html("").outerHeight(0);
				}
			}
		}

		function setCustomEvent(self) {
			head.on("colresize", function(column, e) { // 컬럼 리사이징 관련
				var cols = head.listColumn(),
					bodyCols = body.listColumn(),
					isLast = false;

				for(var j = cols.length - 1; j >= 0; j--) {
					var hw = $(cols[j].element).outerWidth();

					if(self.options.buffer != "page" && cols[j].type == "show" && !isLast) {
						if(_.browser.msie) {
							$(bodyCols[j].element).outerWidth(hw - getScrollBarWidth(self));
						} else {
							$(bodyCols[j].element).css({ "width": "auto" });
						}

						isLast = true;
					} else {
						$(cols[j].element).outerWidth(hw);
						$(bodyCols[j].element).outerWidth(hw);
					}
				}

				reloadScrollWidthResizeBar(500);
				self.emit("colresize", [ column, e ]);
			});

			head.on("colshow", function(column, e) {
				body.uit.showColumn(column.index);
				self.resize();
				self.emit("colshow", [ column, e ]);
			});

			head.on("colhide", function(column, e) {
				body.uit.hideColumn(column.index);
				self.resize();
				self.emit("colhide", [ column, e ]);
			});

            head.on("colclick", function(column, e) {
                self.emit("colclick", [ column, e ]);
            });

            head.on("coldblclick", function(column, e) {
                self.emit("coldblclick", [ column, e ]);
            });

			head.on("colmenu", function(column, e) {
				self.emit("colmenu", [ column, e ]);
			});

			head.on("sort", function(column, e) {
				self.sort(column.index, column.order, false, e);
				self.emit("sort", [ column, e ]);
			});

			body.on("select", function(obj, e) {
				self.emit("select", [ obj, e ]);
			});

			body.on("click", function(obj, e) {
				self.emit("click", [ obj, e ]);
			});

			body.on("dblclick", function(obj, e) {
				self.emit("dblclick", [ obj, e ]);
			});

			body.on("rowmenu", function(obj, e) {
				self.emit("rowmenu", [ obj, e ]);
			});

			body.on("expand", function(obj, e) {
				self.emit("expand", [ obj, e ]);
			});

			body.on("expandend", function(obj, e) {
				self.emit("expandend", [ obj, e ]);
			});
		}

		function setScrollEvent(self, width, height) {
			var opts = self.options;

			var $head = $(self.root).children(".head"),
				$body = $(self.root).children(".body");

			self.addEvent($body, "scroll", function(e) {
				// 컬럼 메뉴는 스크롤시 무조건 숨기기
				self.hideColumnMenu();

				// 가로 스크롤
				if(width > 0) {
					$head.scrollLeft(this.scrollLeft);
				}

				if(opts.buffer == "scroll") { // 무조건 scroll 타입일 때
					var scrollTop = this.scrollTop + height,
						scrollHeight = $body.get(0).scrollHeight;

					if (scrollTop >= scrollHeight * 0.9) {
						self.next();
						self.emit("scroll", e);
					}
				} else if(opts.buffer == "vscroll") {
					if(vscroll_info.prev_scroll_left == this.scrollLeft) {
						renderVirtualScroll(self);

						self.next();
						self.emit("scroll", e);
					} else {
						vscroll_info.prev_scroll_left = this.scrollLeft;
					}
				}

				return false;
			});

			// 스크롤 키보드 이벤트 설정
			if(opts.buffer == "vscroll") {
				$(self.root).hover(function() {
					vscroll_info.is_focus = true;
				}, function() {
					vscroll_info.is_focus = false;
				});

				self.addEvent(document, "keydown", function (e) {
					if(vscroll_info.is_focus) {
						var top = $body.scrollTop(),
							tick = self.options.rowHeight;

						if (e.which == 38 || e.which == 40) {
							$body.scrollTop(top + ((e.which == 38) ? -tick : tick));
						} else if (e.which == 33 || e.which == 34) {
							var newTick = tick * vscroll_info.scroll_count;
							$body.scrollTop(top + ((e.which == 33) ? -newTick : newTick));
						}
					}
				});
			}
		}

        function setScrollWidthResize(self) {
            var column = {},
                width = {},
                resizeX = 0;

            // 리사이즈 엘리먼트 삭제
            $(self.root).find("thead .resize").remove();

            for(var i = 0, len = head.uit.getColumnCount(); i < len; i++) {
                var $colElem = $(head.getColumn(i).element),
                    $resizeBar = $("<div class='resize'></div>");

                var pos = $colElem.position(),
					left = $colElem.outerWidth() + pos.left - 1;

                $resizeBar.css({
                    position: "absolute",
                    width: w_resize + "px",
                    height: $colElem.outerHeight(),
                    left: ((i == len - 1) ? left - w_resize : left) + "px",
                    top: pos.top + "px",
                    cursor: "w-resize",
                    "z-index": "1"
                });

                $colElem.append($resizeBar);

                // Event Start
                (function(index, isLast) {
                    self.addEvent($resizeBar, "mousedown", function(e) {
                        if(resizeX == 0) {
                            resizeX = e.pageX;
                        }

                        // 컬럼 객체 가져오기
                        column = {
                            head: head.getColumn(index),
                            body: body.getColumn(index),
							isLast: isLast
                        };

                        width = {
                            column: $(column.head.element).outerWidth(),
							head: $(head.root).outerWidth(),
                            body: $(body.root).outerWidth(),
							"max-width": parseInt($(head.root).parent().css("max-width"))
                        };

                        is_resize = true;

                        return false;
                    });
                })(i, i == len - 1);
            }

            self.addEvent(document, "mousemove", function(e) {
                if(resizeX > 0) {
                    colResizeWidth(e.pageX - resizeX);
                }
            });

            self.addEvent(document, "mouseup", function(e) {
                if(resizeX > 0) {
					// 마지막 컬럼 크기를 0보다 크게 리사이징시 가로 스크롤 위치 조정
					if(column.isLast) {
						var scrollLeft = $(body.root).parent().scrollLeft(),
							disWidth = e.pageX - resizeX;

						if(disWidth > 0) {
							$(head.root).parent().scrollLeft(scrollLeft + disWidth);
							$(body.root).parent().scrollLeft(scrollLeft + disWidth);
						}
					}

					// 스크롤 위치 초기화
                    resizeX = 0;

                    // 리사이징 바, 위치 이동
					reloadScrollWidthResizeBar(500);
                    head.emit("colresize", [ column.head, e ]);

                	// 리사이징 상태 변경 (delay)
					setTimeout(function() {
						is_resize = false;
					}, 100);

					return false;
				}
            });

            // 리사이징 바 위치 설정
            head.on("colshow", reloadScrollWidthResizeBar);
            head.on("colhide", reloadScrollWidthResizeBar);

            function colResizeWidth(disWidth) {
                var colMinWidth = 30;

				// 전체 최소 크기 체크
				if (width.head + disWidth < width["max-width"]) {
					return;
				}

				// 컬럼 최소 크기 체크
                if (width.column + disWidth < colMinWidth)
                    return;

                $(column.head.element).outerWidth(width.column + disWidth);
                $(column.body.element).outerWidth(width.column + disWidth);

				$(head.root).outerWidth(width.head + disWidth);
				$(body.root).outerWidth(width.body + disWidth);
            }
        }

		function reloadScrollWidthResizeBar(delay) {
			setTimeout(function() {
				for(var i = 0, len = head.uit.getColumnCount(); i < len; i++) {
					var $colElem = $(head.getColumn(i).element);

					var pos = $colElem.position(),
						left = $colElem.outerWidth() + pos.left - 1;

					$colElem.find(".resize").css("left", ((i == len - 1) ? left - w_resize : left) + "px");
				}
			}, delay);
		}

		function getScrollBarWidth(self) {
			return self.options.buffer == "page" ? 0 : _.scrollWidth() + 1;
		}

		function renderVirtualScroll(self) {
			var $viewport = $(self.root).children(".body");
			var viewportHeight = self.options.scrollHeight,
				scrollTop = $viewport.scrollTop(),
				scrollHeight = $viewport[0].scrollHeight;

			// calculate
			var dist = Math.abs(vscroll_info.prev_scroll_top - scrollTop);
			var isDown = vscroll_info.prev_scroll_top < scrollTop;

			var v_height = vscroll_info.height;
			if (dist == 0) {
				return;
			}

			var isBlock = false;
			if (dist < viewportHeight ) {
				isBlock = true;

				// move short dist
				if (dist !== 0) {

					if (dist < v_height) {
						var distIndex = Math.ceil(dist / v_height);
					} else {
						var distIndex = Math.floor(dist / v_height);
					}

					if (isDown == false) {
						vscroll_info.current_row_index -= distIndex;
					} else {
						vscroll_info.current_row_index += distIndex;
					}
				}
			} else {
				// move long dist
				var rate = scrollTop / (scrollHeight - viewportHeight),
					limit = Math.floor(vscroll_info.count - vscroll_info.scroll_count);

				vscroll_info.current_row_index = Math.ceil(limit * rate);
			}

			// traverse real content
			var startIndex = vscroll_info.current_row_index,
				endIndex = startIndex,
				endRowHeight = v_height;

			while(endRowHeight < viewportHeight && endIndex < vscroll_info.count) {
				endIndex++;
				endRowHeight += v_height;
			}

			// 전체 표시 길이가 높이 보다 작을 때
			if (endRowHeight < viewportHeight) {
				// 전체 content 자체가 작다면
				if (viewportHeight > vscroll_info.content_height) {

				} else {
					// 목록이 긴데 마지막이 짧다면
					var hiddenRowCount = Math.ceil(Math.abs(viewportHeight - endRowHeight) / v_height);

					startIndex -= hiddenRowCount;
					endRowHeight += hiddenRowCount * v_height;
				}
			}

			if (startIndex < 0) {
				vscroll_info.prev_scroll_top = 0;
				return;
			}

			// 시작지점으로 스크롤탑 다시 보정.
			if (isBlock) {
				if (endIndex !== vscroll_info.count -1) {
					scrollTop = Math.ceil(startIndex / (vscroll_info.count) * scrollHeight);

					$viewport.scrollTop(scrollTop);
					scrollTop = $viewport.scrollTop();
				}
			}

			var moveHeight = 0;
			var real_viewportHeight = (viewportHeight + vscroll_info.height);
			if (scrollTop >= scrollHeight - real_viewportHeight)  {
				if (endRowHeight > real_viewportHeight) {
					moveHeight = -Math.abs(endRowHeight - real_viewportHeight);
				}
			}

			// save prev scroll top
			vscroll_info.prev_scroll_top = scrollTop;

			// set real content height
			$viewport.css({ "max-height": endRowHeight });
			$(body.root).css({ top: (vscroll_info.prev_scroll_top + moveHeight) + "px" });

			vscroll_info.start_index = startIndex;
			vscroll_info.end_index = endIndex + 1;
		}

		function setVirtualScrollInfo(self) {
			vscroll_info.height = self.options.rowHeight;
			vscroll_info.count = t_rows.length;
			vscroll_info.scroll_count = Math.floor(self.options.scrollHeight / vscroll_info.height);
			vscroll_info.content_height = vscroll_info.count * vscroll_info.height;

			$(body.root).parent().height(vscroll_info.content_height > 0 ? vscroll_info.content_height : "auto");
		}

		function resetVirtualScrollInfo(self) {
			$(self.root).find(".body").scrollTop(0);
			$(body.root).css({ top: "0px" });
            $(body.root).parent().css({ height: "auto" });

			vscroll_info = {
				height: 0,
				content_height: 0,
				count: 0,
				scroll_count: 0,
				prev_scroll_left: 0,
				prev_scroll_top: 0,
				current_row_index: 0,
				start_index: 0,
				end_index: 0,
				is_focus: true
			};
		}

		function setOpenChildRows(rows) {
			for(var i = 0; i < rows.length; i++) {
				t_rows.push(rows[i]);

				if(rows[i].type == "open" && rows[i].children.length > 0) {
					setOpenChildRows(rows[i].children);
				}
			}
		}

		function appendChildRows(p_row, data, type) {
			var no = p_row.children.length,
				c_rows = createRows(_.typeCheck("array", data) ? data : [ data ], no, p_row, type);

			for(var i = 0, len = c_rows.length; i < len; i++) {
				p_row.children.push(c_rows[i]);
			}
		}

		function calculateRows(self, isTree) {
			if(isTree) {
				t_rows = [];
				setOpenChildRows(rows);
			} else {
				t_rows = rows;
			}

			if(self.options.buffer == "vscroll") { // 가상 스크롤 설정
				setVirtualScrollInfo(self);
			}
		}

        function setEventMultiSort(self) {
            var sortIndexes = self.options.msort,
                len = (sortIndexes === true) ? head.uit.getColumnCount() : sortIndexes.length,
				msort_columns = [],
				msort_orders = [];

            for(var i = 0; i < len; i++) {
                var colKey = (sortIndexes === true) ? i : sortIndexes[i],
                    col = self.getColumn(colKey);

                if(col.element != null) {
                    (function(index, column) {
						self.addEvent(column.element, "click", function(e) {
                            if($(e.target).hasClass("resize")) return;

							if(column.order == "asc") {
                            	column.order = null;

                            	for(var j = 0; j < msort_columns.length; j++) {
                            		if(column.name == msort_columns[j]) {
                            			msort_columns.splice(j, 1);
                                        msort_orders.splice(j, 1);
									}
								}
							} else {
								var colIndex = _.inArray(column.name, msort_columns);

								if(column.order == null) {
									column.order = "desc";
								} else if(column.order == "desc") {
									column.order = "asc";
								}

								if(colIndex == -1) {
                                    msort_columns.push(column.name);
                                    msort_orders.push(column.order);
                                } else {
									msort_orders[colIndex] = column.order;
								}
							}

                            self.emit("msort", [ column, e ]);
                            self.msort(msort_columns, msort_orders);
                            self.emit("colclick", [ column, e ]);
                        });
                    })(colKey, col);

                    $(col.element).css("cursor", "pointer");
                }
            }
        }

        function recursiveMultiSort(a, b, columns, order_by, index) {
            var direction = (order_by[index] == "desc") ? 1 : 0,
                key = columns[index],
                is_numeric = !isNaN(+a[key] - +b[key]),
				x = is_numeric ? +a[key] : a[key],
                y = is_numeric ? +b[key] : b[key];

            if(!is_numeric) {
            	if(typeof(x) == "string") x = x.toLowerCase();
            	if(typeof(y) == "string") y = y.toLowerCase();
			}

            if(x < y) {
                return direction == 0 ? -1 : 1;
            }

            if(x == y)  {
                return columns.length-1 > index ? recursiveMultiSort(a,b,columns,order_by,index+1) : 0;
            }

            return direction == 0 ? 1 : -1;
        }

        function printLogForVsInfo(info) {
			if(
				info.start_index < 0 || isNaN(info.start_index) ||
				info.end_index < 0 || isNaN(info.end_index) ||
				info.current_row_index < 0 || isNaN(info.current_row_index) ||
				info.scroll_count < 0 || isNaN(info.scroll_count) ||
				info.prev_scroll_top < 0 || isNaN(info.prev_scroll_top)
			) {
				console.log(info);
			}
		}

		this.init = function() {
			var opts = this.options;

            // @Deprecated, 'rows'는 의미상 맞지 않아 차후 삭제
            opts.data = (opts.rows != null) ? opts.rows : opts.data;

            // 루트가 테이블일 경우, 별도 처리
            if(this.root.tagName == "TABLE") {
                var $root = $(this.root).wrap("<div class='xtable'></div>");
                this.root = $root.parent().get(0);
            }

			// 기본 설정
			createTableList(this);
			setCustomEvent(this);

			// 가로/세로 스크롤 설정
			setScrollEvent(this, opts.scrollWidth, opts.scrollHeight);

			// 멀티소트 이벤트 설정
			if(opts.msort && opts.sortEvent) {
                setEventMultiSort(this);
			}

			// 데이터가 있을 경우
			if(opts.data) {
				this.update(opts.data);
			}

			// 로딩 템플릿 체크 (opts.sortLoading으로 체크하지 않음)
			if(head.tpl["loading"] && modal != null) {
				var $loading = $(head.tpl["loading"]());
				$(this.root).append($loading);

				ui_modal = modal($loading, {
					target: this.selector,
					opacity: 0.1,
					autoHide: false
				});

				// 기본 로딩 시간 (ms)
				opts.sortLoading = (opts.sortLoading === true) ? 500 : opts.sortLoading;
			}

			// 컬럼 리사이징 (기본)
			if(opts.resize) {
				if(opts.scrollWidth > 0) {
					setScrollWidthResize(this);
				} else {
					head.resizeColumns();
					head.resize();
				}
			}
		}

		this.render = function(isTree) {
			calculateRows(this, isTree);
			this.next();
		}

		/**
		 * @method select
		 * Adds a selected class to a row at a specified index and gets an instance of the applicable row.
		 *
		 * @param {Integer} index
		 * @return {RowObject} row
		 */
		this.select = function(index) {
			if(select_row != null) {
				$(select_row.element).removeClass("selected");
			}

			var row = this.get(index);
			select_row = row;

			if(row.element != null) {
				$(row.element).addClass("selected");
			}

			return row;
		}

		/**
		 * @method unselect
		 * Removes a selected class from a selected row and gets an instance of the row in question.
		 *
		 * @return {RowObject} row
		 */
		this.unselect = function() {
			if(select_row != null) {
				$(select_row.element).removeClass("selected");
				select_row = null;
			}
		}

		/**
		 * @method update
		 * Updates the list of rows or modifies the row at a specified index.
		 *
		 * @param {Array} rows
		 */
		this.update = function(dataList) {
			this.reset();
			rows = createRows(dataList, 0, null);

			this.render();
			this.emit("update");
			head.emit("colresize");

			// 정렬 인덱스가 옵션에 있을 경우, 해당 인덱스의 컬럼 정렬 (not loading)
			if(this.options.sortIndex) {
				if(this.options.sort) {
                    this.sort(this.options.sortIndex, this.options.sortOrder, true);
                } else if(this.options.msort) {
                    this.msort(this.options.sortIndex, this.options.sortOrder, true);
				}
			}
		}

		/**
		 * @method updateTree
		 * It is possible to configure a tree table using an object array with the index and data properties.
		 *
		 * @param {Array} rows
		 */
		this.updateTree = function(tree) {
			this.reset();

			for(var i = 0; i < tree.length; i++) {
				var pIndex = iParser.getParentIndex(tree[i].index);

				if(pIndex == null) {
					rows.push(createRows([ tree[i].data ], 0, null, tree[i].type)[0]);
				} else {
					var pRow = this.get(pIndex);

					if(pRow) {
						appendChildRows(pRow, tree[i].data, tree[i].type);
					}
				}
			}

			this.render(true);
			this.emit("updateTree");
		}

		/**
		 * @method append
		 * Add a row or a child row to at a specified index.
		 *
		 * @param {RowObject} row
		 * @param {RowObject} row
		 */
		this.append = function(index, data) {
			var row = this.get(index);

			if(row) {
				appendChildRows(row, data);

				this.clear();
				this.render(true);
				this.emit("append");
			}
		}

		/**
		 * @method open
		 * Shows a child row of a specified index.
		 *
		 * @param {Integer} index
		 */
		this.open = function(index) { // 로트 제외, 하위 모든 노드 대상
			var row = this.get(index);

			if(row) {
				row.type = "open";

				this.clear();
				this.render(true);
				this.emit("open", [row]);
			}
		}

		/**
		 * @method fold
		 * Hides a child row of a specified index.
		 *
		 * @param {Integer} index
		 */
		this.fold = function(index) {
			var row = this.get(index);

			if(row) {
				row.type = "fold";

				this.clear();
				this.render(true);
				this.emit("fold", [row]);
			}
		}

		/**
		 * @method openAll
		 * Shows all child rows of a specified index.
		 */
		this.openAll = function(index) {
			var list = this.getAll(index);

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

				this.clear();
				this.render(true);
				this.emit("openall");
			}
		}

		/**
		 * @method foldAll
		 * Hides all child rows of a specified index.
		 */
		this.foldAll = function(index) {
			var list = this.getAll(index);

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

				this.clear();
				this.render(true);
				this.emit("foldall");
			}
		}

		/**
		 * @method next
		 * Changes to the next page.
		 */
		this.next = function() {
			var start = (page - 1) * this.options.bufferCount,
				end = start + this.options.bufferCount;

			// 가상스크롤일 때만 처리
			if(this.options.buffer == "vscroll") {
				body.reset();

				if(vscroll_info.start_index < vscroll_info.end_index) {
					start = vscroll_info.start_index;
					end = vscroll_info.end_index;
				}

				printLogForVsInfo(vscroll_info);
			}

			// 마지막 페이지 처리
			end = (end > t_rows.length) ? t_rows.length : end;

			if(end <= t_rows.length) {
				var tmpDataList = [];

				for(var i = start; i < end; i++) {
					var r = t_rows[i];

					r.seq = i + 1;
					r.reload(head.uit.getColumn(), null, xss_filter_keys);

					tmpDataList.push(r);
				}

				body.append(tmpDataList);

				// 가상스크롤이 아닐 경우에만 추가
				if(this.options.buffer != "vscroll") {
					this.emit("next", [ page ]);
					if (tmpDataList.length > 0) page++;
				}
			}
		}

		/**
		 * @method page
		 * Changes to the page of at a specified index.
		 *
		 * @param {Integer} index
		 */
		this.page = function(pNo) {
			if(this.options.buffer == "scroll" || this.options.buffer == "vscroll")
				return false;

			if(this.getPage() == pNo) return false;

			this.clear();
			page = (pNo < 1) ? 1 : pNo;
			this.render();
		}

		/**
		 * @method sort
		 * Moves a row iat a specified index to the target index.
		 *
		 * @param {Integer} index
		 * @param {String} order  "asc" or "desc"
		 */
		this.sort = function(index, order, isNotLoading, e) { // index는 컬럼 key 또는 컬럼 name
			if(!this.options.fields || !this.options.sort || this.options.msort || is_resize) return;

			var self = this,
				column = head.getColumn(index);

			if(typeof(column.name) == "string") {
				column.order = (order) ? order : (column.order == "asc" || column.order == null) ? "desc" : "asc";
				head.uit.setColumn(index, column);

				if(this.options.sortLoading && !isNotLoading) {
					self.showLoading();

					setTimeout(function() {
						process();
					}, this.options.sortLoading);
				} else {
					process();
				}
			}

            // 소팅 후, 현재 소팅 상태 캐싱 처리
            if(this.options.sortCache) {
                this.setOption({
                    sortIndex: column.index,
                    sortOrder: column.order
                });
            }

			// 정렬 프로세싱 함수
			function process() {
				rows.sort(function(a, b) {
                    return recursiveMultiSort(a.data, b.data, [ column.name ], [ column.order ], 0);
				});

				// 데이터 초기화 및 입력, 그리고 로딩
				self.clear();
				self.render(true);
				self.emit("sortend", [ column, e ]);
				self.hideLoading();
			}
		}

        /**
         * @method msort
         * Moves a row iat a specified index to the target index.
         *
         * @param {Array} index
         * @param {Array} order  "asc" or "desc"
         */
        this.msort = function(columns, order_by, isNotLoading) {
            if(!this.options.fields || !this.options.msort || this.options.sort) return;
            if(!_.typeCheck("array", columns) || !_.typeCheck("array", order_by) || columns.length != order_by.length) return;

            if(o_rows == null) o_rows = t_rows;
            else t_rows = o_rows;

            var self = this,
                a_rows = t_rows.slice();

            if(this.options.sortLoading && !isNotLoading) {
                self.showLoading();

                setTimeout(function() {
                    process();
                }, this.options.sortLoading);
            } else {
                process();
            }

            // 소팅 후, 현재 소팅 상태 캐싱 처리
            if(this.options.sortCache) {
                this.setOption({
                    sortIndex: columns,
                    sortOrder: order_by
                });
            }

            function process() {
                if(columns.length > 0) {
                    a_rows.sort(function(a, b) {
                        return recursiveMultiSort(a.data, b.data, columns, order_by, 0);
                    });
                }

                // 데이터 초기화 및 입력, 그리고 로딩
                rows = a_rows;
                self.clear();
                self.render(true);
                self.emit("msortend");
                self.hideLoading();
                a_rows = null;
            }
        }

		/**
		 * @method filter
		 * Filters columns at a specified to locate rows that contain keywords in the cell value.
		 *
		 * @param {Function} callback
		 */
        this.filter = function(callback) {
			if(o_rows == null) o_rows = t_rows;
			else t_rows = o_rows;

            var a_rows = t_rows.slice(),
                f_data = [];

            for(var i = 0, len = a_rows.length; i < len; i++) {
				var d = a_rows[i].data;

                if((typeof(callback) == "function" && callback(d) === true) || !callback) {
					f_data.push(d);
                }
            }

            this.update(f_data);
            this.emit("filter", [ f_data ]);
            a_rows = null;
        }

		/**
		 * @method rollback
		 * Returns filtered rows to the original state.
		 */
        this.rollback = function() {
            this.filter(null);
			o_rows = null;
        }

		/**
		 * @method clear
		 * Remove all row elements.
		 */
		this.clear = function() {
			page = 1;
			body.uit.removeRows();
			body.scroll();
		}

		/**
		 * @method clear
		 * Remove all data
		 */
		this.reset = function() {
			if(this.options.buffer == "vscroll") {
				resetVirtualScrollInfo(this);
			}

			this.clear();

			rows = [];
			c_rows = [];
			t_rows = [];
			select_row = null;
		}

		/**
		 * @method resize
		 * Resets the inner scroll and columns of a table.
		 */
		this.resize = function() {
			head.resizeColumns();
			head.resize();
			head.emit("colresize");
		}

		/**
		 * @method scrollWidth
		 * Sets the scroll based on the width of a table.
		 *
		 * @param {Integer} width
		 */
		this.scrollWidth = function(scrollWidth, isInit) {
			// 최초에 스크롤 넓이가 설정되있어야만 메소드 사용 가능
			if(this.options.scrollWidth == 0) return;

			var width = this.options.width;

			if(width > 0) {
				var w = (scrollWidth >= width) ? scrollWidth - getScrollBarWidth(this) : width;
				$(this.root).outerWidth(w);
			} else {
				$(this.root).outerWidth(scrollWidth - getScrollBarWidth(this));
			}

			if(scrollWidth > 0) {
				var originWidth = $(this.root).outerWidth();
				$(this.root).outerWidth(scrollWidth);

				if(isInit) {
					$(head.root).outerWidth(originWidth + getScrollBarWidth(this));
					$(body.root).outerWidth(originWidth);

					reloadScrollWidthResizeBar(1000);
				}

				$(head.root).parent().css("max-width", scrollWidth);
				$(body.root).parent().css("max-width", scrollWidth);
			}
		}

		/**
		 * @method scrollHeight
		 * Sets the scroll based on the height of a table.
		 *
		 * @param {Integer} height
		 */
		this.scrollHeight = function(h) {
			if(this.options.buffer == "page") return;

			$(this.root).find(".body").css("max-height", h + "px");
			this.setOption("scrollHeight", h);

			setScrollEvent(this, this.options.scrollWidth, h);
		}

		/**
		 * @method scrollTop
		 * Sets the scroll based on the height of a table.
		 *
		 * @param {Integer|String} index
		 * @param {Integer} dist
		 */
		this.scrollTop = function(index, dist) {
			if(this.options.buffer != "vscroll") return;

			var $viewport = $(this.root).children(".body");

			// 기존의 로우 그릴 수 있는 형태로 계산하기
			calculateRows(this, true);

			for(var i = 0, len = t_rows.length; i < len; i++) {
				var row = t_rows[i];

				if(("" + index) == row.index) {
					vscroll_info.prev_scroll_top = 0;
					vscroll_info.current_row_index = 0;

					var scrollTop = i * vscroll_info.height,
						scrollHeight = $viewport.height(),
						distTop = _.typeCheck("integer", dist) ? dist : 0;

					if(scrollTop + distTop > scrollHeight) {
						scrollTop += distTop;
					}

					$viewport.scrollTop(scrollTop);
					this.clear();
					this.next();

					break;
				}
			}
		}

		/**
		 * @deprecated
		 * @method height
		 * Sets the scroll based on the height of a table.
		 *
		 * @param {Integer} height
		 */
		this.height = function(h) {
			this.scrollHeight(h);
		}

		/**
		 * @method size
		 * Gets the size of all the rows of a table.
		 *
		 * @return {Integer} size
		 */
		this.size = function() { // 차후 수정 (컬럼 * 로우 개수 * 바이트)
			return rows.length;
		}

		/**
		 * @method count
		 * Gets the number of trows of a table.
		 *
		 * @return {Integer} count
		 */
		this.count = function() {
			return rows.length;
		}

		/**
		 * @method list
		 * Gets all the rows of a table.
		 *
		 * @return {Array} rows
		 */
		this.list = function() {
			return rows;
		}

		/**
		 * @method listColumn
		 * Gets all columns.
		 *
		 * @return {Array} columns
		 */
		this.listColumn = function() {
			return head.listColumn();
		}

		/**
		 * @method listData
		 * Gets the data of all the rows of a table.
		 *
		 * @return {Array} datas
		 */
		this.listData = function() {
			var datas = [];

			for(var i = 0; i < rows.length; i++) {
				datas.push(rows[i].data);
			}

			return datas;
		}

		/**
		 * @method get
		 * Gets the row at the specified index.
		 *
		 * @param {Integer|String} index
		 * @return {RowObject} row
		 */
		this.get = function(index) {
			if(index == null) {
				return null;
			} else {
				var row = c_rows[index];

				if(!row) {
					var keys = iParser.getIndexList(index),
						row = c_rows[keys[0]];

					for(var i = 1, len = keys.length; i < len; i++) {
						if(!row) break;
						row = row.children[keys[i]];
					}

					return row;
				} else {
					return row;
				}
			}
		}

		/**
		 * @method getAll
		 * Gets all rows of at the specified index including child rows.
		 *
		 * @param {Integer} index
		 * @return {Array} rows
		 */
		this.getAll = function(index, _result) {
			var row = this.get(index);

			if(row != null) {
				if(!_.typeCheck("array", _result)) {
					_result = [ row ];
				}

				for(var i = 0; i < row.children.length; i++) {
					var child = row.children[i];
					_result.push(child);

					if(child.children.length > 0) {
						return this.getAll(child.index, _result);
					}
				}
			}

			return _result;
		}

		/**
		 * @method getColumn
		 * Gets the column at the specified index.
		 *
		 * @param {"Integer"/"String"} key index or column key
		 * @return {ColumnObject} column
		 */
		this.getColumn = function(index) {
			return head.getColumn(index);
		}

		/**
		 * @method getData
		 * Gets the data at the specified index.
		 *
		 * @param {"Integer"/"String"} key index
		 * @return {ColumnObject} data
		 */
		this.getData = function(index) {
			var row = this.get(index);
			return (row) ? row.data : null;
		}

		/**
		 * @method showColumn
		 * Shows the column index (or column name).
		 *
		 * @param {"Integer"/"String"} key index or column name
		 */
		this.showColumn = function(index) {
			head.showColumn(index);
		}

		/**
		 * @method hideColumn
		 * Hides the column index (or column name).
		 *
		 * @param {"Integer"/"String"} key index or column name
		 */
		this.hideColumn = function(index) {
			head.hideColumn(index);
		}

		/**
		 * @method initColumns
		 * It is possible to determine the index or name of the column to be shown in an array.
		 *
		 * @param {"Integer"/"String"} key index or column name
		 */
		this.initColumns = function(keys) {
			head.initColumns(keys);
			body.initColumns(keys);
			head.emit("colresize");
		}

		/**
		 * @method showColumnMenu
		 * Shows the Show/Hide Column menu at specified coordinates.
		 *
		 * @param {Integer} x
		 * @param {Integer} y
		 */
		this.showColumnMenu = function(x, y) {
			head.showColumnMenu(x, y);
		}

		/**
		 * @method hideColumnMenu
		 * Hides the Show/Hide Column menu.
		 */
        this.hideColumnMenu = function() {
            head.hideColumnMenu();
        }

		/**
		 * @method toggleColumnMenu
		 * Shows or hides the Show/Hide Column menu.
		 *
		 * @param {Integer} x
		 * @param {Integer} y
		 */
        this.toggleColumnMenu = function(x, y) {
			head.toggleColumnMenu(x, y);
        }

		/**
		 * @method showExpand
		 * Shows the extended row area of a specified index.
		 *
		 * @param {Integer} index
		 */
		this.showExpand = function(index, obj) {
			body.showExpand(index, obj);
		}

		/**
		 * @method hideExpand
		 * Hides the extended row area of a specified index.
		 */
		this.hideExpand = function(index) {
			if(index) body.hideExpand(index);
			else body.hideExpand();
		}

		/**
		 * @method getExpand
		 * Get a row in which the extended area is currently activated.
		 *
		 * @return {RowObject} row
		 */
		this.getExpand = function() {
			return body.getExpand();
		}

		/**
		 * @method showLoading
		 * Shows the loading screen for the specified delay time.
		 *
		 * @param {Integer} delay
		 */
		this.showLoading = function(delay) {
			if(!ui_modal || is_loading) return;

			ui_modal.show();
			is_loading = true;

			if(delay > 0) {
				var self = this;

				setTimeout(function() {
					self.hideLoading();
				}, delay);
			}
		}

		/**
		 * @method hideLoading
		 * Hides the loading screen.
		 */
		this.hideLoading = function() {
			if(!ui_modal || !is_loading) return;

			ui_modal.hide();
			is_loading = false;
		}

        /**
         * @method isLoading
         */
        this.isLoading = function() {
            return is_loading;
        }

		/**
		 * @method setCsv
		 * Updates a table using a CVS string.
		 */
		this.setCsv = function(csv) {
            var opts = this.options;
			if(!opts.fields && !opts.csv) return;

			var fields = _.getCsvFields(opts.fields, opts.csv),
                csvNumber = (opts.csvNumber) ? _.getCsvFields(opts.fields, opts.csvNumber) : null;

			this.update(_.csvToData(fields, csv, csvNumber));
		}

		/**
		 * @method setCsvFile
		 * Updates a table using a CVS file.
		 */
		this.setCsvFile = function(file) {
			if(!this.options.fields && !this.options.csv) return;

			var self = this;
			_.fileToCsv(file, function(csv) {
	            self.setCsv(csv);
			});
		}

		/**
		 * @method getCsv
		 * Gets the data of a table as a CSV string.
		 *
		 * @param {Boolean} isTree
		 * @return {String} csv
		 */
		this.getCsv = function() {
			if(!this.options.fields && !this.options.csv) return;

			var fields = _.getCsvFields(this.options.fields, this.options.csv),
				len = (rows.length > this.options.csvCount) ? this.options.csvCount : rows.length;

			return _.dataToCsv2({
				fields: fields,
				rows: this.listData(),
				count: len,
				names: this.options.csvNames
			});
		}

		/**
		 * @method getCsvBase64
		 * Gets the data of a table as a CSV string encoded as base64.
		 *
		 * @param {Boolean} isTree
		 * @return {String} base64
		 */
		this.getCsvBase64 = function() {
			if(!this.options.fields && !this.options.csv) return;

			return _.csvToBase64(this.getCsv());
		}

		/**
		 * @method downloadCsv
		 * Downloads the data of a table as a CSV file.
		 *
		 * @param {String} name
		 * @param {Boolean} isTree
		 */
        this.downloadCsv = function(name) {
            if(_.typeCheck("string", name)) {
                name = name.split(".")[0];
            }

            var a = document.createElement('a');
            a.download = (name) ? name + ".csv" : "table.csv";
            a.href = this.getCsvBase64();

            document.body.appendChild(a);
            a.click();
            a.parentNode.removeChild(a);
        }

		/**
		 * @method rowFunc
		 * Ir is possible to use a function for all row data applicable to the column (or column name) of a specified column (or column name). Currently only SUM and AVG are supported.
		 *
		 * @param {"sum"/"svg"} funcType
		 * @param {Integer} columnIndex
		 * @param {Function} callback
		 */
		this.rowFunc = function(type, index, callback) {
			if(!this.options.fields) return;

			var isCallback = (typeof(callback) == "function") ? true : false;
			var result = 0,
				count = (isCallback) ? 0 : rows.length,
				column = head.getColumn(index);

			if(column.name) {
				for(var i = 0; i < rows.length; i++) {
					var data = rows[i].data,
						value = data[column.name];

					if(!isNaN(value)) {
						if(isCallback) {
							if(callback(rows[i])) {
								result += value;
								count++;
							}
						} else {
							result += value;
						}
					}
				}
			}

			// 현재는 합계와 평균만 지원함
			if(type == "sum") return result;
			else if(type == "avg") return result / count;

			return null;
		}

		/**
		 * @method getPage
		 * Gets the current page of a table.
		 *
		 * @return {Integer} page
		 */
		this.getPage = function() {
			return page - 1;
		}

		/**
		 * @method activeIndex
		 * Gets the index of a row that is activated in an extended/modified/selected state.
		 *
		 * @return {Integer} index
		 */
		this.activeIndex = function() {
			if(!select_row) return null;
			return select_row.index;
		}
	}

    UI.setup = function() {
        return {
			/**
			 * @cfg {Array} [fields=null]
			 * Sets the name of columns in the order of being displayed on the table screen.
			 */
			fields: null,

			/**
			 * @cfg {Array} [csv=null]
			 * Sets the column key shown when converted to a CSV string.
			 */
			csv: null,

			/**
			 * @cfg {Array} [csvNames=null]
			 * Sets the name of a column shown when converting to a CSV string, which must be defined in the same order as the CSV option.
			 */
			csvNames: null,

			/**
			 * @cfg {Array} [csvNumber=null]
			 * Sets the column key to be changed to a number form when converted to a CSV string.
			 */
			csvNumber: null,

			/**
			 * @cfg {Array} [csvCount=10000]
			 * Sets the maximum number of rows when creating a CSV string.
			 */
			csvCount: 10000,

			/**
			 * @cfg {Array} data
			 * Sets the initial row list of a table.
			 */
			data: [],

			/**
			 * @cfg {Array} rows
			 * Sets the initial row list of a table (@Deprecated).
			 */
			rows: null, // @Deprecated

			/**
			 * @cfg {Boolean/Array} [colshow=false]
			 * Sets a column index shown when the Show/Hide Column menu is enabled.
			 */
			colshow: false,

			/**
			 * @cfg {Boolean} [expand=false]
			 * Determines whether to use an extended row area.
			 */
			expand: false,

			/**
			 * @cfg {Boolean} [expandEvent=true]
			 * Shows the extended area automatically when clicking on a row.
			 */
			expandEvent: true,

			/**
			 * @cfg {Boolean} [resize=false]
			 * Determines whether to use the column resizing function.
			 */
			resize: false,

			/**
			 * @cfg {Integer} [rowHeight=26]
			 * Sets the reference height of a body area when using a table scroll.
			 */
			rowHeight: 26,

			/**
			 * @cfg {Integer} [scrollHeight=200]
			 * Sets the reference height of a body area when using a table scroll.
			 */
			scrollHeight: 200,

			/**
			 * @cfg {Integer} [scrollWidth=0]
			 * Sets the reference width of a body area when using a table scroll.
			 */
			scrollWidth: 0,

			/**
			 * @cfg {Integer} [width=0]
			 * Sets the area of a table.
			 */
			width: 0,

			/**
			 * @cfg {String} [buffer='scroll'/'page'/'s-page'/'vscroll']
			 * Sets the buffer type of a table.
			 */
			buffer: "scroll",

			/**
			 * @cfg {Integer} [bufferCount=100]
			 * Sets the number of rows per page.
			 */
			bufferCount: 100,

            /**
             * @cfg {Boolean/Array} [msort=false]
             * Determines whether to use the table sort function.
             */
            msort: false,

			/**
			 * @cfg {Boolean/Array} [sort=false]
			 * Determines whether to use the table sort function.
			 */
			sort: false,

			/**
			 * @cfg {Boolean} [sortLoading=false]
			 * Determines whether to show the loading screen when sorting a table.
			 */
			sortLoading: false,

			/**
			 * @cfg {Boolean} [sortCache=false]
			 * Configures settings to ensure that the sort state can be maintained even when the table is updated.
			 */
			sortCache: false,

			/**
			 * @cfg {Integer} [sortIndex=null]
			 * Determines whether to use the table sort function.
			 */
			sortIndex: null,

			/**
			 * @cfg {String} [sortOrder="asc"]
			 * Determines whether to use the table sort function.
			 */
			sortOrder: "asc",

			/**
			 * @cfg {Boolean} [sortEvent=true]
			 * Determines whether to use the sort function when you click on a column.
			 */
			sortEvent: true,

            /**
             * @cfg {Boolean} [xssFilter=false]
             * Activate the xss filter to set the column value.
             */
            xssFilter: false,

            /**
             * @cfg {Boolean} [animate=false]
             * @deprecated
             */
            animate: false
        }
    }

	/**
	 * @event select
	 * Event that occurs when a row is selected (@Deprecated)
	 *
	 * @param {RowObject) row
	 * @param {EventObject} e The event object
	 */

	/**
	 * @event click
	 * Event that occurs when a row is clicked
	 *
	 * @param {RowObject) row
	 * @param {EventObject} e The event object
	 */

	/**
	 * @event dblclick
	 * Event that occurs when a row is double clicked
	 *
	 * @param {RowObject) row
	 * @param {EventObject} e The event object
	 */

	/**
	 * @event sort
	 * Event that occurs when the table is sorted.
	 *
	 * @param {ColumnObject) column
	 * @param {EventObject} e The event object
	 */

	/**
	 * @event scroll
	 * Event that occurs when the scroll of a table is located at the lowermost position.
	 *
	 * @param {EventObject} e The event object
	 */

	/**
	 * @event rowmenu
	 * Event that occurs when a row is right clicked.
	 *
	 * @param {RowObject) row
	 * @param {EventObject} e The event object
	 */

	/**
	 * @event colclick
	 * Event that occurs when a column is clicked.
	 *
	 * @param {ColumnObject) column
	 * @param {EventObject} e The event object
	 */

	/**
	 * @event colshow
	 * Event that occurs when shown column is selected.
	 *
	 * @param {ColumnObject) column
	 * @param {EventObject} e The event object
	 */

	/**
	 * @event colhide
	 * Event that occurs when hidden column is selected.
	 *
	 * @param {ColumnObject) column
	 * @param {EventObject} e The event object
	 */

	/**
	 * @event colresize
	 * Event that occurs when the column resizing is activated.
	 *
	 * @param {ColumnObject) column
	 * @param {EventObject} e The event object
	 */

	/**
	 * @event expand
	 * Event that occurs when the extended row area is enabled.
	 *
	 * @param {RowObject) row
	 * @param {EventObject} e The event object
	 */

	/**
	 * @event expandend
	 * Event that occurs when the extended row area is disabled.
	 *
	 * @param {RowObject) row
	 * @param {EventObject} e The event object
	 */

	return UI;
});