/*
 * Copyright (c)  2006-2007 Maskat Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
maskat.lang.Class.declare("maskat.xml.ElementBinding", {

	/**
	 * コンストラクタ
	 *
	 * @param binder このバインディングを使用する XMLObjectBinder
	 * @param name XML 要素の名前空間 URI
	 * @param name XML 要素のローカル名
	 * @param config バインディング設定
	 *     {
	 *        type: XML 要素の読み込み時に生成するオブジェクトの型
	 *
	 *        attributes:
	 *            プロパティ名 = XML 属性名
	 *            プロパティ値 = XML 属性のバインディング設定
	 *                           詳細は maskat.xml.AttributeBinding を参照
	 *
	 *        children:
	 *            プロパティ名 = 子要素の XML 要素名
	 *            プロパティ値 = 子要素のバインディング設定 
	 *                           詳細は addChildBinding メソッドを参照
	 *     }
	 */
	initialize: function(binder, uri, name, config) {
		this.binder = binder;
		this.uri = uri || "";
		this.name = name;
		this.type = config && config.type;
		this.children = {};
		this.attributes = {};

		if (config && config.attributes) {
			for (var attrName in config.attributes) {
				this.addAttributeBinding(attrName, config.attributes[attrName]);
			}
		}

		if (config && config.children) {
			for (var childName in config.children) {
				this.addChildBinding(childName, config.children[childName]);
			}
		}
	},

	addAttributeBinding: function(name, config) {
		var binding = new maskat.xml.AttributeBinding(name, config);
		this.attributes[name] = binding;
	},

	getChildBinding: function(uri, name) {
		if (typeof(uri) == "object") {
			var qname = uri;
			uri = qname.namespaceURI;
			name = qname.localName || qname.baseName || qname.nodeName;
		}

		var binding = this.children[name] || this.children["*"];
		if (!binding) {
			throw new maskat.lang.Error("INVALID_CHILD_ELEMENT",
				{ elementName: this.name, childName: name });
		}
		return binding;
	},

	addChildBinding: function(name, config) {
		var uri = (name.charAt(0) == "#") ? "" : this.uri;
		var binding = new maskat.xml.ChildNodeBinding(this.binder, uri, name, config);
		this.children[name] = binding;
	},

	/**
	 * XML 要素の情報をオブジェクトに読み込み、そのオブジェクトを返します。
	 *
	 * @param object 読み込み先のオブジェクト
	 *        null または undefined の場合は新しいオブジェクトを生成する
	 * @param element XML 要素
	 *
	 * @return XML 要素の情報を読み込んだオブジェクト
	 */
	read: function(object, element) {
		object = object || this.createObject(element);

		/* 属性の読み込み */
		var attributes = element.attributes;
		for (var i = 0; i < attributes.length; i++) {
			if (attributes[i].prefix == "xmlns") {
				/* XML 名前空間宣言を登録する */
				var attrName = attributes[i].localName || attributes[i].baseName;
				this.binder.addPrefixMapping(attrName, attributes[i].nodeValue);
			} else if (attributes[i].nodeName == "xmlns") {
				/* デフォルトの XML 名前空間宣言を登録する */
				this.binder.addPrefixMapping("", attributes[i].nodeValue);
			} else {
				/* その他の属性を読み込み */
				try {
					this.readAttribute(object, attributes[i]);
				} catch (e) {
					throw new maskat.lang.Error("ATTRIBUTE_READ_ERROR", {
						elementName: this.name,
						attributeName: attributes[i].nodeName
					}, e);
				}
			}
		}

		/* 省略された属性の読み込み (必須チェック／デフォルト値) */
		for (var name in this.attributes) {
			if (name != "*" && !element.getAttribute(name)) {
				try {
					this.attributes[name].read(object, null);
				} catch (e) {
					throw new maskat.lang.Error("ATTRIBUTE_READ_ERROR", {
						elementName: this.name,
						attributeName: name
					}, e);
				}
			}
		}

		/* 子要素の読み込み */
		var children = element.childNodes;
		var occurrence = {};
		for (var j = 0; j < children.length; j++) {
			/* 子要素の出現回数をカウントする */
			var childName = children[j].localName || children[j].baseName || children[j].nodeName;
			occurrence[childName] = (occurrence[childName] || 0) + 1;
		
			switch (children[j].nodeType) {
			case 1: /* Node.ELEMENT_NODE */
			case 4: /* Node.CDATA_SECTION_NODE */
				this.readChildElement(object, children[j]);
				break;
			case 3: /* Node.TEXT_NODE */
				if (!children[j].nodeValue.match(/^\s*$/)) {
					this.readChildElement(object, children[j]);
				}
				break;
			}
		}

		/* 子要素の出現回数のチェック */
		for (var name in this.children) {
			var config = this.children[name];
			if (config.required && !occurrence[name]) {
				/* 必須の子要素が省略されている場合はエラー */
				throw new maskat.lang.Error("MISSING_CHILD_ELEMENT", {
					elementName: this.name, childName: name	});
			}
			if (!config.repeat && occurrence[name] > 1) {
				/* 繰り返しできない子要素が繰り返された場合はエラー */
				throw new maskat.lang.Error("DUPLICATED_CHILD_ELEMENT", {
					elementName: this.name, childName: name	});
			}
		}

		return object;
	},

	/**
	 * XML 要素の読み込み先として使用する新しいオブジェクトを生成します。
	 *
	 * @param element XML 要素
	 *
	 * @return オブジェクト
	 */
	createObject: function(element) {
		try {
			if (this.type) {
				return new this.type();
			}
			return this.binder.createObject(element);
		} catch (e) {
			throw new maskat.lang.Error("ELEMENT_READ_ERROR",
				{ elementName: this.name }, e);
		}
	},

	readAttribute: function(object, attribute) {
		var name = attribute.localName || attribute.baseName;
		var binding = this.attributes[name];
		if (binding) {
			binding.read(object, attribute);
		} else if (this.attributes["*"]) {
			object[name] = attribute.nodeValue;
		} else {
			throw new maskat.lang.Error("UNKNOWN_ATTRIBUTE",
				{ elementName: this.name, attributeName: name });
		}
	},

	readChildElement: function(object, element) {
		var binding = this.getChildBinding(element);
		binding.read(object, element);
	},

	/**
	 * オブジェクトを XML 要素の形式で文字列バッファに書き出します。
	 *
	 * @param object オブジェクト
	 * @param buffer 文字列バッファ
	 */
	write: function(object, buffer) {
		var prefix = this.binder.getPrefix(this.uri);
		var nodeName = prefix ? (prefix + ":" + this.name) : this.name; 

		/* XML 要素の開始タグと属性を出力 */
		buffer.push("<" + nodeName);
		
		for (var attrName in this.attributes) {
			this.writeAttribute(attrName, object, buffer);
		}
		buffer.push(">");
		var length = buffer.length;

		/* 子要素を出力 */
		for (var name in this.children) {
			if (this.children[name].required && !object[name]) {
				throw new maskat.lang.Error("MISSING_CHILD_ELEMENT", {
					elementName: this.name, childName: name	});
			}
			this.writeChildElement(name, object, buffer);
		}

		/* 終了タグを出力 */
		if (buffer.length == length) {
			buffer[length - 1] = "/>";
		} else {
			buffer.push("</" + nodeName + ">");
		}
	},
	
	writeAttribute: function(name, object, buffer) {
		var binding = this.attributes[name];
		if (!binding) {
			throw new maskat.lang.Error("UNKNOWN_ATTRIBUTE",
				{ elementName: this.name, attributeName: name });
		}
		binding.write(object, buffer);
	},

	writeChildElement: function(name, object, buffer) {
		var binding = this.getChildBinding("", name);
		binding.write(object, buffer);
	}

});
