/**********************************************************************************
 * modular_model.js
 * 
 * Modular 프레임워크의 모델과 관련된 부분이 포함되어 있는 스크립트 파일
 * 'Modular.model' 이라는 네임스페이스를 사용하고 있음
 * 
 * @author 김선엽(sunyoupk@udapsoft.co.kr)
 **********************************************************************************/

/*=============================================================
 * 
 * SearchOperator 구현
 * 
 =============================================================*/

/**
 * 조회 연산자를 포함하고 있는 객체
 */
Modular.model.SearchOperator = {
	GREATER : ">",
	GREATER_EQUAL : ">=",
	LESS : "<",
	LESS_EQUAL : "<=",

	LIKE_BOTH : "LIKE_BOTH",
	LIKE_NONE : "LIKE_NONE",
	LIKE_LEFT : "LIKE_LEFT",
	LIKE_RIGHT : "LIKE_RIGHT",
	BETWEEN : "BETWEEN",
	EQUALS : "=",
	IN : "IN",
	IS_NULL : "IS NULL",

	NOT_LIKE_BOTH : "NOT LIKE_BOTH",
	NOT_LIKE_NONE : "NOT LIKE_NONE",
	NOT_LIKE_LEFT : "NOT LIKE_LEFT",
	NOT_LIKE_RIGHT : "NOT LIKE_RIGHT",
	NOT_BETWEEN : "NOT BETWEEN",
	NOT_EQUALS : "<>",
	NOT_IN : "NOT IN",
	IS_NOT_NULL : "IS NOT NULL",

	/**
	 * 비교 연산자 배열
	 */
	compareOperator: new Array(),
	/**
	 * 기간 연산자 배열
	 */
	rangeOperator: new Array(),
	/**
	 * 복수 연산자 배열
	 */
	multiOperator: new Array(),

	/**
	 * 연산자 유형 별로 분류 작업을 수행하는 초기화 함수
	 */
	initialize : function() {
		this.compareOperator.push(this.GREATER);
		this.compareOperator.push(this.GREATER_EQUAL);
		this.compareOperator.push(this.LESS);
		this.compareOperator.push(this.LESS_EQUAL);
		this.compareOperator.push(this.LIKE_BOTH);
		this.compareOperator.push(this.LIKE_NONE);
		this.compareOperator.push(this.LIKE_LEFT);
		this.compareOperator.push(this.LIKE_RIGHT);
		this.compareOperator.push(this.EQUALS);
		this.compareOperator.push(this.IS_NULL);
		this.compareOperator.push(this.NOT_LIKE_BOTH);
		this.compareOperator.push(this.NOT_LIKE_NONE);
		this.compareOperator.push(this.NOT_LIKE_LEFT);
		this.compareOperator.push(this.NOT_LIKE_RIGHT);
		this.compareOperator.push(this.NOT_EQUALS);
		this.compareOperator.push(this.IS_NOT_NULL);

		this.rangeOperator.push(this.BETWEEN);
		this.rangeOperator.push(this.NOT_BETWEEN);

		this.multiOperator.push(this.IN);
		this.multiOperator.push(this.NOT_IN);
	},

	/**
	 * 주어진 연산자가 비교 연산자인지를 리턴합니다.
	 * 
	 * @param op {Modular.model.SearchOperator} 연산자
	 * @return {boolean} 비교 연산자 여부
	 */
	isCompareOperator : function(op) {
		if (Modular.Utils.hasText(op)) {
			for (var i =0; i < this.compareOperator.length; i++) {
				if (this.compareOperator[i].toLowerCase() == op.toLowerCase()) {
					return true;
				}//end if
			}//end for
		}//end if
		return false;
	},

	/**
	 * 주어진 연산자가 기간 연산자인지를 리턴합니다.
	 * 
	 * @param op {Modular.model.SearchOperator} 연산자
	 * @return {boolean} 기간 연산자 여부
	 */
	isRangeOperator : function(op) {
		if (Modular.Utils.hasText(op)) {
			for (var i =0; i < this.rangeOperator.length; i++) {
				if (this.rangeOperator[i].toLowerCase() == op.toLowerCase()) {
					return true;
				}//end if
			}//end for
		}//end if
		return false;
	},

	/**
	 * 주어진 연산자가 복수 연산자인지를 리턴합니다.
	 * 
	 * @param op {Modular.model.SearchOperator} 연산자
	 * @return {boolean} 복수 연산자 여부
	 */
	isMultiOperator : function(op) {
		if (Modular.Utils.hasText(op)) {
			for (var i =0; i < this.multiOperator.length; i++) {
				if (this.multiOperator[i].toLowerCase() == op.toLowerCase()) {
					return true;
				}//end if
			}//end for
		}//end if
		return false;
	}
};

Modular.model.SearchOperator.initialize();


/*=============================================================
 * 
 * Condition 구현
 * 
 =============================================================*/

/**
 * Condition 생성자 함수
 * 
 * @param key {String} 조회조건 키
 * @param operator {Modular.model.SearchOperator} 조회조건 연산자
 * @param value {String} 조회조건 값
 * @param mapping {String} 조회조건 자동 매핑 여부   
 */
Modular.model.Condition = function (key, operator, value, mapping) {
	this.key = key;
	this.operator = Modular.Utils.hasText(operator) ? operator : Modular.model.SearchOperator.EQUALS;	
	this.value = value || "";
	this.type = "normal";
	this.mapping = mapping;

	if (!Modular.model.SearchOperator.isCompareOperator(this.operator)) {
		throw new Error( "[" + operator + "]는 지원하지 않는 연산자 입니다." );
	}//end if
};//end of constructor


/*=============================================================
 * 
 * RangeCondition 구현
 * 
 =============================================================*/

/**
 * RangeCondition 생성자 함수
 * 
 * @param key {String} 조회조건 키
 * @param operator {Modular.model.SearchOperator} 조회조건 연산자
 * @param fKey {String} 조회조건 시작 값의 키
 * @param tKey {String} 조회조건 종료 값의 키
 * @param mapping {String} 조회조건 자동 매핑 여부 
 */
Modular.model.RangeCondition = function (key, operator, fKey, tKey, mapping) {
	this.key = key;
	this.operator = Modular.Utils.hasText(operator) ? operator : Modular.model.SearchOperator.BETWEEN;
	this.fKey = fKey;
	this.tKey = tKey;
	this.fValue = "";
	this.tValue = "";
	this.type = "range";
	this.mapping = mapping;
	
	if (!Modular.model.SearchOperator.isRangeOperator(this.operator)) {
		throw new Error( "[" + operator + "]는 지원하지 않는 연산자 입니다.");
	}//end if
};//end of constructor


/*=============================================================
 * 
 * MultiCondition 구현
 * 
 =============================================================*/

/**
 * MultiCondition 생성자 함수
 * 
 * @param key {String} 조회조건 키
 * @param operator {Modular.model.SearchOperator} 조회조건 연산자
 * @param mapping {String} 조회조건 자동 매핑 여부 
 */
Modular.model.MultiCondition = function (key, operator, mapping) {
	this.key = key;
	this.operator = Modular.Utils.hasText(operator) ? operator : Modular.model.SearchOperator.IN;
	this.value = [];
	this.type = "multi";
	this.mapping = mapping;
	
	if (!Modular.model.SearchOperator.isMultiOperator(this.operator)) {
		throw new Error("[" + operator + "]는 지원하지 않는 연산자 입니다.");
	}//end if
};//end of constructor


/*=============================================================
 * 
 * SearchCondition 구현
 * 
 =============================================================*/

/**
 * SearchCondition 생성자 함수
 * 
 * @param id {String} 조회조건 ID
 */
Modular.model.SearchCondition = function (id) {
	this.id = id;
	this.sessionKey = Modular.model.PageContext.SESSION_KEY_PAGE_SEARCH_CONDITION_BACKUP_KEY_PREFIX + Modular.model.PageContext.REQUEST_URI + ":" + this.id;
	this.conditions = {};	
};//end of constructor

/**
 * 주어진 Condition을 추가합니다.
 * 
 * @param condition {Modular.model.Condition} Condition 객체
 */
Modular.model.SearchCondition.prototype.addCondition = function (condition) {
	this.conditions[condition.key] = condition;
};

/**
 * 주어진 id에 해당하는 Condition 객체를 리턴합니다.
 * 
 * @param key {String} Condition의 키 값
 */
Modular.model.SearchCondition.prototype.getCondition = function (key) {
	return this.conditions[key];
};

/**
 * 주어진 key에 해당하는 Condition이 존재하는지 여부를 리턴합니다.
 * 
 * @param key {String} Condition의 키 값
 * @return {boolean} Condition 존재 여부
 */
Modular.model.SearchCondition.prototype.hasCondition = function (key) {
	if (this.conditions[key]) {
		return true;
	} else {
		return false;
	}//end if else
};

/**
 * 주어진 key에 해당하는 Condition에 주어진 값을 설정합니다.
 * 
 * @param key {String} Condition의 키 값
 * @param value {Object} Condition의 값
 */
Modular.model.SearchCondition.prototype.setCondition = function (key, value) {
	this.conditions[key].value = value;
};



/*=============================================================
 * 
 * Column 구현
 * 
 =============================================================*/


/**
 * 컬럼 타입을 나타내는 상수
 */
Modular.model.ColumnType = {
	/**
	 * 문자열 타입
	 */
	STRING : "string",
	/**
	 * 날짜 타입
	 */
	DATE : "date",
	/**
	 * 숫자 타입
	 */
	NUMBER : "number",
	/**
	 * boolean 타입
	 */
	BOOLEAN : "boolean"
};

/**
 * Column 생성자 함수
 * 
 * @param id {String} 컬럼 ID
 * @param type {Modular.model.ColumnType} 컬럼 타입
 */
Modular.model.Column = function (id, type) {	
	this.id = id;
	this.type = type;
};//end of constructor


/*=============================================================
 * 
 * Record 구현
 * 
 =============================================================*/

/**
 * 레코드 상태를 나타내는 상수
 * 
 * TODO 서버의 상수와 동기화를 위해 JSP 형태로 변경하는 것을 고려
 * 
 * 2008.11.21 - Helexis
 */
Modular.model.RecordStatus = {
	/**
	 * 레코드 상태 : 정상 - 아무일도 일어나지 않았음
	 */
	NORMAL : "N",
	/**
	 * 레코드 상태 : 추가 - 신규 추가된 레코드 임
	 */
	INSERTED : "I",
	/**
	 * 레코드 상태 : 수정 - 수정된 레코드 임
	 */
	UPDATED : "U",
	/**
	 * 레코드 상태 : 삭제 - 삭제된 레코드 임
	 */
	DELETED : "D"
};

/**
 * Record 생성자 함수
 * 
 * @param id {String} 레코드 ID
 * @param value {Array} 레코드 값. Array 형태임.
 * @param status {Modular.model.RecordStatus} 레코드 상태 값
 */
Modular.model.Record = function (id, value, status) {
	this.id = id;
	this.cell = value || [];
	this.status = status || Modular.model.RecordStatus.NORMAL;
};//end of constructor

/**
 * 주어진 인덱스에 해당하는 컬럼 값을 리턴합니다.
 * 
 * @param id {Number} 컬럼 인덱스
 * @return 컬럼 값
 */
Modular.model.Record.prototype.getColumn = function (id) {
	if (Modular.Utils.hasText(id)) {
		var num = new Number(id);
		if (isNaN(num)) {
			return null;
		} else {
			// 인덱스라면, 그냥 컬럼 배열에서 꺼내준다.
			return this.cell[id];
		}//end if else
	} else {
		return null;
	}//end if
};

/**
 * 주어진 인덱스와 값을 지정된 컬럼에 설정합니다.
 * 
 * @param idx 컬럼 인덱스
 * @param value 컬럼 값
 */
Modular.model.Record.prototype.setColumn = function (idx, value) {
	var num = new Number(idx);
	if (isNaN(num)) {
		// do nothing...
	} else {
		// 인덱스라면, 그냥 컬럼을 찾아서 설정한다.
		this.cell[idx] = value;
	}//end if else
};

/**
 * 레코드가 가지는 컬럼 값을 객체의 형태로 만들어서 리턴합니다.
 * 
 * @param columns {Array} 컬럼 배열
 * @return {Object}레코드의 ID와 값을 포함하는 객체
 */
Modular.model.Record.getDataRow = function (columns, cell) {
	var row = {};
	jQuery.each(columns, function(idx, column) {
		row[column.name] = cell[idx];
	});
	return row;
};



/*=============================================================
 * 
 * RecordSet 구현
 * 
 =============================================================*/

/**
 * RecordSet 생성자 함수
 * 
 * 파라미터로 받는 객체 metadata는 아래와 같은 프로퍼티를 포함할 수 있습니다.
 * 
 *  - bindingClass {Object} 서버의 DTO 바인딩을 위한 클래스
 *  - columns {Array} 레코드의 컬럼 메타 정보 배열
 *  - volumePerPage {Number} 페이지 당 게시물 수
 *  - currentPage {Number} 현재 페이지
 *  - totalRecordSize {Number} 전체 게시물 수
 * 
 * @param id {String} 레코드 셋 ID
 * @param metadata {Object} 레코드의 메타 정보를 포함하는 객체
 * @param records {Array} 레코드의 객체의 Array
 */
Modular.model.RecordSet = function (id, metadata, records) {
	this.id = id;
	this.metadata = jQuery.extend({
		/**
		 * 레코드 컬럼 정보
		 */
		columns : [],
		/**
		 * 레코드 셋이 바인딩되는 DTO 클래스 : 기본 값은 java.util.HashMap
		 * 
		 * 기본 값을 java.util.Map으로 하는 이유는, DTO 없이 개발이 가능하도록 하기 위함임.
		 */
		bindingClass : "java.util.HashMap",
		/**
		 * 페이지 당 게시물 수 : 기본 값은 10
		 */
		volumePerPage : 10,
		/**
		 * 현재 페이지 : 기본 값은 1
		 */
		currentPage : 1,
		/**
		 * 전체 게시물 수
		 */
		totalRecordCount : 0,
		/**
		 * 테이블 내부의 페이징이나 정렬 기능을 사용할 경우, 호출할 조회 컨트롤 ID
		 */
		searchControlId : "",
		/**
		 * 정렬 대상 컬럼
		 */
		sortColumnId : "",
		/**
		 * 정렬 대상 컬럼 정렬순서
		 */
		sortOrder : true,
		/**
		 * 응답을 처리하기 위한 핸들러
		 */
		responseHandler : false,
		
		/**
		 * 레코드 셋의 처리 결과를 담는 객체
		 */
		processResult : {}
	}, metadata || {});
	this.records = records || [];
};//end of constructor

/**
 * 주어진 ID 혹은 인덱스에 해당하는 레코드를 리턴합니다.
 * 
 * @param id {String} or {Number} 레코드 ID 혹은 인덱스
 * @return {Modular.model.Record} Record 객체
 */
Modular.model.RecordSet.prototype.getRecord = function (id) {
	if (Modular.Utils.hasText(id)) {
		var num = new Number(id);
		if (isNaN(num)) {
			// TODO 성능 개선 필요
			for (var i = 0; i < this.records.length; i++) {
				if (this.records[i].id == id) {
					return this.records[i];
				}//end if
			}//end for
			return null;
		} else {
			// 인덱스라면, 그냥 레코드 배열에서 꺼내준다.
			return this.records[id];
		}//end if else
	} else {
		if (isNaN(id)) {
			return null;
		}//end if
		return this.records[id];
	}//end if		
};

/**
 * 레코드를 추가합니다.
 * 
 * @param id {String} 레코드 ID
 * @param value {Array} 레코드 값. Array 형태임.
 * @param status {Modular.model.RecordStatus} 레코드 상태 값
 * @return {Modular.model.Record} 추가된 레코드 객체
 */
Modular.model.RecordSet.prototype.addRecord = function (id, value, status) {
	if (this.getRecord(id)) {
		/*
		 * TODO 비즈니스 로직적으로 그냥 오류를 발생시키고
		 * 말아도 되는지 확인해 보도록!
		 * 
		 * 2008.11.11 - Helexis
		 */
		throw new Error("[" + id + "]에 해당하는 레코드가 이미 존재합니다.");
	}//end if
	var stat = status || Modular.model.RecordStatus.INSERTED;
	var record = new Modular.model.Record(id, value, stat);
	this.records.push(record);
	return record;
};

/**
 * 주어진 ID에 해당하는 레코드를 삭제합니다.
 * 
 * @param id {String} 레코드 ID
 */
Modular.model.RecordSet.prototype.deleteRecord = function (id) {
	this.getRecord(id).status = Modular.model.RecordStatus.DELETED;
};


/**
 * 레코드 셋이 가지는 모든 레코드를 객체의 배열 형태로 만들어서 리턴합니다.
 * 
 * @param recordSet {Modular.model.RecordSet} 레코드 셋 객체
 * @return {Array} 레코드의 ID와 값을 포함하는 객체 배열
 */
Modular.model.RecordSet.getDataRows = function (recordSet) {
	var rows = [];
	var columns = recordSet.metadata.columns;
	var records = recordSet.records;
	jQuery.each(records, function (index, record) {
		rows.push(Modular.model.Record.getDataRow(columns, record.cell));
	});
	return rows;
};


/*=============================================================
 * 
 * PageContext 구현
 * 
 =============================================================*/


/**
 * 페이지 내에서 존재하는 모델을 포함하는 PageContext 객체를 생성합니다.
 */
Modular.model.PageContext = {
	/**
	 * 컨텍스트 ROOT 경로를 나타내는 상수
	 */
	CONTEXT_ROOT : "",
	/**
	 * 페이지 요청 URI를 나타내는 상수
	 */
	REQUEST_URI : "",
	/**
	 * 페이지 내부의 초기화 함수 목록
	 */
	initFunctions : [],
	/**
	 * 페이지 내부에서 사용하는 코드 목록
	 */
	codeContext : {},
	/**
	 * 페이지 내부에서 사용하는 validator 목록
	 */
	validators : [],
	/**
	 * 페이지 내부에서 사용하는 그리드에 열 추가 시
	 * 임시로 사용할 레코드의 시퀀스
	 */
	gridTempRecordSequence : 0,
	/**
	 * 페이지 내부의 SearchCondition 목록
	 */
	searchConditions : {},
	/**
	 * 페이지 내부의 RecordSet 목록
	 */
	recordSets : {},
	/**
	 * 페이지 내부의 Control 목록
	 */
	controls : {},
	/**
	 * 페이지 네비게이션 히스토리 목록
	 */
	navigationHistoryFunctions : [],
	/**
	 * 페이지 내부의 파일 업로드 컴포넌트 목록
	 */
	fileSets : {},
	/**
	 * 페이지 내부의 파일 업로드 컴포넌트 뷰 목록
	 */
	fileSetViews : {},
	/**
	 * 조회조건에 대한 폼 값 바인딩 여부
	 */
	popSearchForm : true,
	/**
	 * 페이지 네비게이션 설정 데이터
	 */
	navigation : {},
	/**
	 * 주어진 ID에 해당하는 SearchCondition 객체를 리턴합니다.
	 * 
	 * @param id SearchCondition ID
	 * @return SearchCondition 객체
	 */
	getSearchCondition : function (id) {
		return Modular.model.PageContext.searchConditions[id];
	},
	/**
	 * 주어진 ID에 해당하는 RecordSet 객체를 리턴합니다.
	 * 
	 * @param id RecordSet ID
	 * @return RecordSet 객체
	 */
	getRecordSet : function (id) {
		return Modular.model.PageContext.recordSets[id].recordSet;
	},
	/**
	 * 주어진 ID에 해당하는 RecordSet 객체의 초기화 함수 목록를 리턴합니다.
	 * 
	 * @param id RecordSet ID
	 * @return 초기화 함수 목록
	 */
	getRecordSetInitFunctions : function (id) {
		return Modular.model.PageContext.recordSets[id].initFunctions;
	},
	/**
	 * 주어진 ID에 해당하는 Control 객체를 리턴합니다.
	 * 
	 * @param id Control ID
	 * @return Control 객체
	 */
	getControl : function (id) {
		return Modular.model.PageContext.controls[id];
	},
	/**
	 * 주어진 객체를 PageContext 에 추가합니다.
	 * 
	 * @param t {Object} PageContext 객체에 추가할 SearchCondition/RecordSet/Control 객체
	 */
	add : function (t) {
		if (t instanceof Modular.model.SearchCondition) {
			Modular.model.PageContext.searchConditions[t.id] = t;
		} else if (t instanceof Modular.model.RecordSet) {
			Modular.model.PageContext.recordSets[t.id] = {
				recordSet : t,
				initFunctions : []
			};
		} else if (t instanceof Modular.model.Control) {
			Modular.model.PageContext.controls[t.id] = t;
		}//end if else
	}
};


/*=============================================================
 * 
 * Control 구현
 * 
 =============================================================*/

/**
 * 컨트롤을 생성하기 위한 생성자
 * 
 * 옵션으로 다음과 같은 내용이 포함될 수 있습니다.
 *  - id 컨트롤 ID
 *  - eventHandler 컨트롤의 이벤트 핸들러 함수
 *  - nextControl 다음 실행 컨트롤. 주로, 저장 후 재 조회 시 사용.
 * 
 * @param o {Object} 옵션 내역을 포함하는 객체
 */
Modular.model.Control = function (o) {
	var option = o || {};
	this.id = option.id;
	this.chainControl = option.chainControl;
	this.eventHandler = null;
	this.url = "";
	this.successMessage = option.successMessage;
	this.commands = [];
};

/**
 * 컨트롤에 커맨드를 추가합니다.
 * 
 * @param c 커맨드
 */
Modular.model.Control.prototype.add = function (c) {
	if (c && c instanceof Modular.model.Command) {
		this.commands.push(c);
	}//end if
};


/*=============================================================
 * 
 * Command 구현
 * 
 =============================================================*/

/**
 * 커맨드 타입을 나타내는 상수
 */
Modular.model.CommandType = {
	/**
	 * 조회 타입
	 */
	SEARCH : "search",
	/**
	 * 추가 타입
	 */
	INSERT : "insert",
	/**
	 * 삭제 타입
	 */
	DELETE : "delete",
	/**
	 * 수정 타입
	 */
	UPDATE : "update",
	/**
	 * 저장 타입
	 */
	SAVE : "save"
};

/**
 * 커맨드를 생성합니다.
 * 
 * @param {Object} 옵션 객체
 */
Modular.model.Command = function (option) {
	this.id = option.id;
	this.type = option.type;
	this.i = option.i;
	this.o = option.o;
	this.preHandle = option.preHandle;
	this.searchCondition = null;
	this.recordSets = {};
};

/**
 * 커맨드 전송 전 준비 작업을 수행합니다.
 */
Modular.model.Command.prototype.prepare = function () {
	var command = this;
	switch (this.type) {
		case Modular.model.CommandType.SEARCH :
			/*
			 * 조회일 경우에는 불필요하게 RecordSet 내부의 데이터가 전송되지 않도록,
			 * RecordSet의 메타 정보만 복사하여 전송한다.
			 */
			Modular.view.SearchView.setAllConditions(this.i);
			this.searchCondition = Modular.model.PageContext.getSearchCondition(this.i);
			jQuery.each(this.o, function (n, val) {
				var rs = Modular.model.PageContext.getRecordSet(val);
				command.recordSets[rs.id] = new Modular.model.RecordSet(rs.id, rs.metadata);
			});
			break;
		default :
			var rs = Modular.model.PageContext.getRecordSet(this.i);
			this.recordSets[rs.id] = rs;
	}//end switch case
};


/*=============================================================
 * 
 * ServiceExecutionContext 구현
 * 
 =============================================================*/

/**
 * 컨트롤 객체를 사용하여 ServiceExecutionContext 객체를 초기화 합니다.
 * 
 * @param control 컨트롤 객체
 */
Modular.model.ServiceExecutionContext = function (control, flag) {
	jQuery.each(control.commands, function (n, command) {
		if (!flag) {
			command.preHandle.call(this);
		}//end if
		command.prepare();
	});
	this.commands = control.commands;
};

