package appengine.util;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;

import com.google.appengine.api.datastore.Blob;
import com.google.appengine.api.datastore.DatastoreFailureException;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.DatastoreTimeoutException;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Query;
import com.google.apphosting.api.DeadlineExceededException;

/**
 * @author shin1ogawa
 */
public class DatastoreServiceUtil {

	static final Logger LOGGER = Logger.getLogger(DatastoreServiceUtil.class.getName());


	private DatastoreServiceUtil() {
	}


	/**
	 * @author shin1ogawa
	 */
	public static class RetryCounterExceededException extends RuntimeException {

		private static final long serialVersionUID = 4260381175607811492L;


		/**
		 * the constructor.
		 * @category constructor
		 */
		public RetryCounterExceededException() {
			super("リトライの上限回数を超えて失敗しました.");
		}
	}


	/**
	 * リトライを行う{@link DatastoreService#put(Entity)}.
	 * @param entity
	 * @param count
	 * @return {@link Key}
	 * @see DatastoreService#put(Entity)
	 */
	public static Key putWithRetry(Entity entity, int count) {
		for (int i = 0; i < count; i++) {
			try {
				Key key = DatastoreServiceFactory.getDatastoreService().put(entity);
				return key;
			} catch (DatastoreTimeoutException e) {
				continue;
			} catch (DatastoreFailureException e) {
				continue;
			}
		}
		throw new RetryCounterExceededException();
	}

	/**
	 * リトライを行う{@link DatastoreService#delete(Key...)}.
	 * @param key
	 * @param count
	 */
	public static void deleteWithRetry(Key key, int count) {
		for (int i = 0; i < count; i++) {
			try {
				DatastoreServiceFactory.getDatastoreService().delete(key);
				return;
			} catch (DatastoreTimeoutException e) {
				continue;
			} catch (DatastoreFailureException e) {
				continue;
			}
		}
		throw new RetryCounterExceededException();
	}

	/**
	 * ルートキーに属するEntityGroupのキーのリストを返す。
	 * @param service 
	 * @param rootKey
	 * @param kinds 走査するKind
	 * @return ルートキーに属するEntityGroupのキーのリスト
	 */
	public static List<Key> getEntityGroupKey(DatastoreService service, Key rootKey, String[] kinds) {
		List<Key> keys = new ArrayList<Key>();
		for (String kind : kinds) {
			List<Entity> list =
					service.prepare(
							new Query(kind, rootKey).setKeysOnly().addSort(
									Entity.KEY_RESERVED_PROPERTY)).asList(
							FetchOptions.Builder.withOffset(0));
			for (Entity entity : list) {
				keys.add(entity.getKey());
			}
		}
		return keys;
	}

	/**
	 * @param kind
	 * @param maxCount 削除する最大件数。おもに{@link DeadlineExceededException}への対策のため。
	 * @return 削除した件数
	 */
	public static int deleteKind(String kind, int maxCount) {
		long start = System.currentTimeMillis();
		if (maxCount == 0) {
			return 0;
		}
		if (StringUtils.isEmpty(kind)) {
			throw new IllegalArgumentException("kindが指定されていません。");
		}
		DatastoreService service = DatastoreServiceFactory.getDatastoreService();
		int count = 0;
		Iterator<Entity> iterator = service.prepare(new Query(kind).setKeysOnly()).asIterator();
		for (int i = 0; i < maxCount && iterator.hasNext(); i++) {
			Entity entity = iterator.next();
			deleteWithRetry(entity.getKey(), 5);
			count++;
			if (System.currentTimeMillis() - start > 250000) {
				// 25秒経過したら停止する。
				break;
			}
		}
		return count;
	}

	/**
	 * バックアップ用のKindに、複数の{@link Entity}を一括で保存する。
	 * <p>ひとつRootEntityを作成し、バックアップ対象は全てその子EntityとしてひとつのEntityGroupを保存する。</p>
	 * @param service
	 * @param backupKind
	 * @param entities
	 * @return バックアップしたEntityGroupのルート{@link Entity}
	 * @throws IOException
	 */
	public static Key backupEntities(DatastoreService service, String backupKind,
			Collection<Entity> entities) throws IOException {
		Key backupKey = service.allocateIds(backupKind, 1).getStart();
		int size = entities.size();
		List<Entity> backupEntities = new ArrayList<Entity>(size + 1);
		Entity backupRoot = new Entity(backupKey);
		backupEntities.add(backupRoot);
		Iterator<Key> keyIterator = service.allocateIds(backupKey, backupKind, size).iterator();
		for (Entity entity : entities) {
			Entity backup = new Entity(keyIterator.next());
			backup.setProperty("key", entity.getKey());
			backup.setUnindexedProperty("serializedEntity", new Blob(getBytes(entity)));
			backupEntities.add(backup);
		}
		DatastoreTransactionUtil.runInTransaction(new DatastoreTransactionUtil.PutAllInTransaction(
				backupEntities), 5);
		return backupKey;
	}

	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();
	}
}
