package com.shin1ogawa.entity;

import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;

import org.apache.commons.lang.RandomStringUtils;
import org.junit.Before;
import org.junit.Test;

import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.shin1ogawa.AbstractRelasionShipTest;

public class PersonTest extends AbstractRelasionShipTest {

	@Before
	public void setUp() {
		super.setUp();
		createRandomPerson(2000);
	}

	@Test
	public void countの速度を確認() {
		final String parameter = "180.0";
		count1(parameter); // キャッシュの問題でこいつの結果は無視する。
		count2(parameter);
		count1(parameter);
		count2(parameter);
	}

	public void count1(String parameter) {
		long before0, before, after;
		PersistenceManager pm = getFactory().getPersistenceManager();
		pm.setIgnoreCache(true);
		Query query = pm.newQuery(Person.class);
		query.setOrdering("height asc, key asc");
		query.setFilter("height >= pHeight");
		query.declareParameters("java.lang.Double pHeight");
		before0 = System.currentTimeMillis();
		before = before0;
		@SuppressWarnings("unchecked")
		List<Person> list = (List<Person>) query.execute(Double
				.parseDouble(parameter));
		after = System.currentTimeMillis();
		// System.out.println("count1:query.execute() ms=" + (after - before));
		before = System.currentTimeMillis();
		int size = list.size();
		after = System.currentTimeMillis();
		System.out.println("count1:size()=" + size + ", ms=" + (after - before)
				+ ", total ms=" + (after - before0));
		query.closeAll();
		pm.close();
	}

	public void count2(String parameter) {
		long before, after;
		PersistenceManager pm = getFactory().getPersistenceManager();
		pm.setIgnoreCache(true);
		Query query = pm.newQuery(Person.class);
		query.setOrdering("height asc, key asc");
		query.setFilter("height >= pHeight");
		query.declareParameters("java.lang.Double pHeight");
		query.setResult("count(this)");
		before = System.currentTimeMillis();
		int size = (Integer) query.execute(Double.parseDouble(parameter));
		after = System.currentTimeMillis();
		System.out
				.println("count2:size()=" + size + ", total ms=" + (after - before));
		query.closeAll();
		pm.close();
	}


	/**
	 * ソート対象が重複する可能性がある場合。
	 */
	@Test
	public void ソート順を利用したページング() {
		int page = 1;
		int countInPage = 300;
		Person next = null;
		int totalCount = 0; // 総処理件数

		while (true) {
			PersistenceManager pm = getFactory().getPersistenceManager();
			Query query = pm.newQuery(Person.class);
			query.setOrdering("height asc, key asc");
			PagingResult result;
			if (page > 1) {
				// まずは重複する可能性のある箇所をequality filterして取得する。
				Query dupQuery = pm.newQuery(Person.class);
				dupQuery.setFilter("height == pHeight && key >= pKey"); // '=='を使う。
				dupQuery
						.declareParameters("java.lang.Double pHeight, java.lang.String pKey");
				PagingResult dupResult = getListInPage(countInPage, dupQuery,
						next.getHeight(), next.getKey());
				if (dupResult.hasNext) {
					// 重複したソートキーだけで1ページ分取得できた。
					result = dupResult;
				} else {
					// 1ページ分に足りないので続きを取得する。
					query.setFilter("height > pHeight"); // '>='ではなく'>'を使う。
					query.declareParameters("java.lang.Double pHeight");
					result = getListInPage(countInPage
							- dupResult.getList().size(), query, next
							.getHeight());
					// resultに重複分を追加する。
					result.getList().addAll(0, dupResult.getList());
				}
			} else {
				result = getListInPage(countInPage, query);
			}
			List<Person> list = result.getList();
			// 2000 / 300 なので7ページ目のときは200件
			assertThat(list.size(), is(equalTo(page < 7 ? 300 : 200)));
			totalCount += list.size();
			if (page > 1) {
				// 前のページで取得した「次のページの最初のEntity」が正しく働いているか？
				assertThat(list.get(0).getKey(), is(equalTo(next.getKey())));
			}
			query.closeAll();
			pm.close();
			if (result.hasNext()) {
				// 次のページの最初に来るべきEntity(次のページのクエリで指定する)を保存する。
				next = result.getNext();
				page = page + 1;
			} else {
				// 次のページが無い。
				break;
			}
		}
		assertThat(page, is(equalTo(7)));
		assertThat(totalCount, is(equalTo(2000)));
	}

	/**
	 * @param countInPage
	 *            1ページに表示する件数
	 * @param query
	 *            フィルタやソート順を設定済みの{@link Query}
	 * @return
	 */
	private PagingResult getListInPage(int countInPage, Query query,
			Object... params) {
		query.setRange(0, countInPage + 1); // 1件多く取得する
		@SuppressWarnings("unchecked")
		List<Person> list1 = params != null && params.length > 0 ? (List<Person>) query
				.executeWithArray(params)
				: (List<Person>) query.execute();
		if (list1.size() > countInPage) {
			return new PagingResult(new ArrayList<Person>(list1.subList(0,
					countInPage)), list1.get(countInPage));
		} else {
			return new PagingResult(new ArrayList<Person>(list1), null);
		}
	}

	static class PagingResult {
		private final List<Person> list;
		private final Person next;
		private final boolean hasNext;

		public PagingResult(List<Person> list, Person firstPersonInNextPage) {
			super();
			this.list = list;
			this.next = firstPersonInNextPage;
			hasNext = firstPersonInNextPage != null;
		}

		/**
		 * @return the existsNext
		 */
		public boolean hasNext() {
			return hasNext;
		}

		/**
		 * @return the firstPersonInNextPage
		 */
		public Person getNext() {
			return next;
		}

		/**
		 * @return the list
		 */
		public List<Person> getList() {
			return list;
		}
	}

	private void createRandomPerson(int count) {
		Entity[] entities = new Entity[count];
		for (int i = 0; i < count; i++) {
			String name = RandomStringUtils.randomAlphabetic(10);
			double height = Math.round(Math.random() * 100 + 100);
			double weight = Math.random() * 50 + 40;
			Calendar birth = Calendar.getInstance();
			birth.add(Calendar.DATE, (int) (Math.random() * 365 * 100 * -1));

			entities[i] = new Entity(Person.class.getSimpleName());
			entities[i].setProperty("name", name);
			entities[i].setProperty("height", height);
			entities[i].setProperty("weight", weight);
			entities[i].setProperty("birth", birth.getTime());
		}

		DatastoreService service = DatastoreServiceFactory
				.getDatastoreService();
		service.put(Arrays.asList(entities));
	}
}
