package appengine.util;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.google.appengine.api.datastore.Blob;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.Text;
import com.google.appengine.repackaged.com.google.common.util.Base64;
import com.google.appengine.repackaged.com.google.common.util.Base64DecoderException;

/**
 * 主に{@link Entity}をXmlに出力、またはその逆を行うためのユーティリティクラス。
 * @author shin1ogawa
 */
public class DatastoreXmlUtil {

	static final Logger logger = Logger.getLogger(DatastoreXmlUtil.class.getName());

	static {
		logger.setLevel(Level.FINEST);
	}


	private DatastoreXmlUtil() {
	}

	/**
	 * Xml形式のバイト配列を読み込んで{@link Entity}を作成する。
	 * <p><pre>
	 * &lt;entities&gt;
	 *   &lt!-- Keyは自動採番する --&gt;
	 *   &lt;entity kind="Kind"&gt;
	 *   &lt;property name="property1" type="java.lang.String"&gt;値&lt;/property&gt;
	 *   &lt;/entity&gt;
	 *   &lt!-- Keyは名前付きで作成する --&gt;
	 *   &lt;entity kind="Kind" <strong>key-name="name"</strong>&gt;
	 *   &lt;property name="property1" type="java.lang.String"&gt;値&lt;/property&gt;
	 *   &lt;/entity&gt;
	 *   &lt!-- Keyはエンコード済みのキーをデコードして作成する --&gt;
	 *   &lt;entity kind="Kind"&gt;
	 *   <strong>&lt;encoded-pkey"&gt;xxxxxxxxxxxxx&lt;/encoded-pkey&gt;</strong>
	 *   &lt;property name="property1" type="java.lang.String"&gt;値&lt;/property&gt;
	 *   &lt;/entity&gt;
	 *   &lt!-- Keyは名前付き、エンコード済みの親キーを関連づけて作成する --&gt;
	 *   &lt;entity kind="Kind" <strong>key-name="name"</strong>&gt;
	 *   <strong>&lt;ancestor-key"&gt;xxxxxxxxxxxxx&lt;/ancestor-key&gt;</strong>
	 *   &lt;property name="property1" type="java.lang.String"&gt;値&lt;/property&gt;
	 * &lt;/entities&gt;
	 * </pre></p>
	 * @param input
	 * @return Xmlを読み込んで作成した{@link Entity}のリスト
	 * @throws SAXException
	 * @throws IOException
	 * @throws ParserConfigurationException
	 * @throws ClassNotFoundException
	 * @throws Base64DecoderException
	 */
	public static List<Entity> readFromXml(InputStream input) throws SAXException, IOException,
			ParserConfigurationException, ClassNotFoundException, Base64DecoderException {
		Document root = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(input);
		Node entitiesNode = root.getFirstChild();
		NodeList entitiesChildList = entitiesNode.getChildNodes();
		int entitiesChildCount = entitiesChildList.getLength();
		List<Entity> newEntities = new ArrayList<Entity>();
		for (int i = 0; i < entitiesChildCount; i++) {
			Node entityChildNode = entitiesChildList.item(i);
			if (entityChildNode.getNodeType() != Node.ELEMENT_NODE) {
				continue;
			}
			String nodeName = entityChildNode.getNodeName();
			if (!"entity".equals(nodeName)) {
				continue;
			}
			Node kindAttrNode = entityChildNode.getAttributes().getNamedItem("kind");
			String kind = kindAttrNode != null ? kindAttrNode.getNodeValue() : null;
			Node keyNameAttrNode = entityChildNode.getAttributes().getNamedItem("key-name");
			String keyName = keyNameAttrNode != null ? keyNameAttrNode.getNodeValue() : null;
			if (StringUtils.isEmpty(kind)) {
				continue;
			}
			logger.finest("entity: kind=" + kind);
			Map<String, Object> map = new HashMap<String, Object>();
			NodeList entityChildList = entityChildNode.getChildNodes();
			int entityChildCount = entityChildList.getLength();
			Key primaryKey = null, ancestorKey = null;
			for (int j = 0; j < entityChildCount; j++) {
				Node entityChild = entityChildList.item(j);
				if (entityChild.getNodeType() != Node.ELEMENT_NODE) {
					continue;
				}
				nodeName = entityChild.getNodeName();
				String value = entityChild.getTextContent();
				if ("property".equals(nodeName)) {
					Node typeAttrNode = entityChild.getAttributes().getNamedItem("type");
					String typeName = typeAttrNode != null ? typeAttrNode.getNodeValue() : null;
					Node propertyNameAttrNode = entityChild.getAttributes().getNamedItem("name");
					String propertyName =
							propertyNameAttrNode != null ? propertyNameAttrNode.getNodeValue()
									: null;
					logger.finest("  property: name=" + propertyName + ", type=" + typeName
							+ ", value=" + value);
					map.put(propertyName, getObjectValue(typeName, value));
				} else if ("encoded-pkey".equals(nodeName)) {
					logger.finest("  encoded-pkey: value=" + value);
					primaryKey = KeyFactory.stringToKey(value);
				} else if ("ancestor-key".equals(nodeName)) {
					logger.finest("  ancestor-key: value=" + value);
					ancestorKey = KeyFactory.stringToKey(value);
				}
			} // for (int j = 0; j < entityChildCount; j++) {
			Entity entity = null;
			if (primaryKey != null) {
				entity = new Entity(primaryKey);
			} else if (ancestorKey != null) {
				entity = new Entity(kind, ancestorKey);
			} else if (StringUtils.isNotEmpty(keyName)) {
				entity = new Entity(kind, keyName);
			} else {
				entity = new Entity(kind);
			}
			Iterator<Entry<String, Object>> properties = map.entrySet().iterator();
			while (properties.hasNext()) {
				Entry<String, Object> next = properties.next();
				entity.setProperty(next.getKey(), next.getValue());
			}
			newEntities.add(entity);
		}
		return newEntities;
	}

	/**
	 * Kindを指定して、Kindが保持する全てのEntityをXml形式で出力する。
	 * @param writer
	 * @param kind
	 * @throws IOException
	 */
	public static void writeToXml(PrintWriter writer, String kind) throws IOException {
		Iterator<Entity> i =
				DatastoreServiceFactory.getDatastoreService().prepare(new Query(kind)).asIterator();
		writer.println("<entities>");
		while (i.hasNext()) {
			writeToXml(writer, i.next());
			writer.flush();
		}
		writer.println("</entities>");
	}

	/**
	 * {@link Entity}をXml形式で出力する。
	 * <p>TODO Entity, Entity[] children を作れたら良いのだが。孫レベルはどーするか？とか。</p>
	 * @param writer
	 * @param entity
	 * @throws IOException
	 */
	public static void writeToXml(PrintWriter writer, Entity entity) throws IOException {
		Key key = entity.getKey();
		if (StringUtils.isEmpty(key.getName())) {
			writer.println(String.format("<entity kind=\"%s\" id=\"%s\">", entity.getKind(), key
				.getId()));
		} else {
			writer.println(String.format("<entity kind=\"%s\" name=\"%s\">", entity.getKind(), key
				.getName()));
		}
		writer.println(String.format(" <!-- %s -->", key.toString()));
		writer.println(String.format(" <encoded-pkey>%s</encoded-pkey>", KeyFactory
			.keyToString(key)));
		if (key.getParent() != null) {
			writer.println(String.format(" <ancestor-key>%s</ancestor-key>", KeyFactory
				.keyToString(key.getParent())));
		}
		Iterator<Entry<String, Object>> i = entity.getProperties().entrySet().iterator();
		while (i.hasNext()) {
			Entry<String, Object> next = i.next();
			String name = next.getKey();
			Object value = next.getValue();
			if (value == null) {
				writer.println(String.format(" <property name=\"%s\" type=\"null\"></property>",
						name));
				continue;
			}
			writer.println(String.format(" <property name=\"%s\" type=\"%s\">%s</property>", name,
					value.getClass().getName(), getStringValue(value)));
		}
		writer.println("</entity>");
	}

	static Object getObjectValue(String type, String stringValue) throws ClassNotFoundException,
			IOException, Base64DecoderException {
		if (Text.class.getName().equals(type)) {
			return new Text(stringValue);
		}
		if (Blob.class.getName().equals(type)) {
			return new Blob(Base64.decode(stringValue));
		}
		if (Long.class.getName().equals(type)) {
			return Long.valueOf(stringValue);
		}
		if (Key.class.getName().equals(type)) {
			return KeyFactory.stringToKey(stringValue);
		}
		if (Integer.class.getName().equals(type)) {
			return Integer.valueOf(stringValue);
		}
		if (Boolean.class.getName().equals(type)) {
			return Boolean.valueOf(stringValue);
		}
		if (String.class.getName().equals(type)) {
			return stringValue;
		}
		return getObject(Base64.decode(stringValue));
	}

	static String getStringValue(Object value) throws IOException {
		if (value instanceof Text) {
			return ((Text) value).getValue();
		}
		if (value instanceof Blob) {
			return Base64.encode(((Blob) value).getBytes());
		}
		if (value instanceof Key) {
			return KeyFactory.keyToString((Key) value);
		}
		if (value instanceof Long || value instanceof Integer || value instanceof String
				|| value instanceof Boolean) {
			return String.valueOf(value);
		}
		return Base64.encode(getBytes(value));
	}

	static Object getObject(byte[] bytes) throws ClassNotFoundException, IOException {
		ByteArrayInputStream input = new ByteArrayInputStream(bytes);
		ObjectInputStream ois = null;
		try {
			ois = new ObjectInputStream(input);
			return ois.readObject();
		} finally {
			IOUtils.closeQuietly(ois);
		}
	}

	static byte[] getBytes(Object value) throws IOException {
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		ObjectOutputStream os = null;
		try {
			os = new ObjectOutputStream(bos);
			os.writeObject(value);
		} finally {
			IOUtils.closeQuietly(os);
		}
		return bos.toByteArray();
	}
}
