package com.yoda.yodao.internal;

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

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;

import org.junit.Assert;

import com.yoda.yodao.annotation.Repository;
import com.yoda.yodao.internal.query.YoGroupBy;
import com.yoda.yodao.internal.query.YoHaving;
import com.yoda.yodao.internal.query.YoOrderBy;
import com.yoda.yodao.internal.query.YoQuery;
import com.yoda.yodao.internal.query.YoOrderBy.Order;
import com.yoda.yodao.internal.query.YoQuery.CRUD;
import com.yoda.yodao.internal.query.YoSelection;

public class RepositoryParser {

	public static DaoInfo parser(Element element) throws ProcessException {
		DaoInfo daoInfo = new DaoInfo();
		Repository repository = element.getAnnotation(Repository.class);
		String elementName = element.asType().toString();

		daoInfo.setDaoClass(Utils.parseClassNameToClazz(elementName));

		boolean isInterface = element.getKind().isInterface();
		daoInfo.setIsInterface(isInterface);
		TypeMirror genericType = null;
		TypeMirror pkType = null;
		if (isInterface) {
			List<? extends TypeMirror> interfaces = ((TypeElement) element)
					.getInterfaces();
			if (interfaces == null || interfaces.size() == 0) {
				throw new ProcessException(element, elementName
						+ " must to be a interface.");
			}
			genericType = Utils.getGenericType(interfaces.get(0), 0);
			pkType = Utils.getGenericType(interfaces.get(0), 1);
		} else {
			TypeMirror superclass = ((TypeElement) element).getSuperclass();
			genericType = Utils.getGenericType(superclass, 0);
			pkType = Utils.getGenericType(superclass, 1);
			Set<Modifier> modifiers = ((TypeElement) element).getModifiers();
			if (modifiers.contains(Modifier.ABSTRACT)) {

			}
		}
		if (genericType == null) {
			throw new ProcessException(element, elementName
					+ " must has a generic type which it's the entity class");
		}
		if (pkType == null) {
			throw new ProcessException(element, elementName
							+ " must has a generic type which it's the entity's PK class");
		}
		String className = genericType.toString();
		Clazz entityClass = Utils.parseClassNameToClazz(className);
		daoInfo.setEntityClass(entityClass);
		daoInfo.setPkClass(pkType.toString());

		List<DaoMethod> methods = parserMethods(element);
		if (methods != null) {
			for (DaoMethod method : methods) {
				YoQuery query = parseDaoMethodToQuery(method);
				method.setQuery(query);
			}
		}
		daoInfo.setMethods(methods);

		return daoInfo;
	}

	private static boolean isAbstract(ExecutableElement e) {
		return e != null && e.getModifiers().contains(Modifier.ABSTRACT);
	}

	private static List<DaoMethod> parserMethods(Element element) {
		boolean isInterface = element.getKind().isInterface();
		List<DaoMethod> methods = new ArrayList<DaoMethod>();
		List<? extends Element> elems = element.getEnclosedElements();
		for (Element e : elems) {
			if (e instanceof ExecutableElement
					&& e.getKind() == ElementKind.METHOD) {
				ExecutableElement executableElement = (ExecutableElement) e;
				TypeElement enclosingElement = (TypeElement) e
						.getEnclosingElement();
				if (isInterface || isAbstract(executableElement)) {
					DaoMethod method = new DaoMethod();
					String methodName = executableElement.getSimpleName()
							.toString();
					method.setMethodName(methodName);
					String returnType = executableElement.getReturnType()
							.toString();
					method.setReturnType(returnType);
					com.yoda.yodao.annotation.Query query = executableElement
							.getAnnotation(com.yoda.yodao.annotation.Query.class);
					if (query != null) {
						method.setSql(query.value());
					}

					List<? extends VariableElement> parameters = executableElement
							.getParameters();
					int count = parameters.size();
					if (parameters != null && count > 0) {
						DaoParam[] params = new DaoParam[count];
						for (int i = 0; i < count; i++) {
							VariableElement param = parameters.get(i);
							String paramType = param.asType().toString();
							String paramName = param.getSimpleName().toString();
							params[i] = new DaoParam(paramName, paramType);
						}
						method.setMethodParams(params);
					}

					method.setElement(e);
					methods.add(method);
				}
			}
		}

		return methods;
	}

	private static final String READ_PREFIX = "find";
	private static final String READ_ONE_PREFIX = "findOne";
	private static final String READ_LIST_PREFIX = "findList";
	private static final String CREATE_PREFIX = "save";
	private static final String UPDATE_PREFIX = "update";
	private static final String DELETE_PREFIX = "delete";
	private static final String COUNT_PREFIX = "count";

	private static final String KEYWORD_BY = "By";
	private static final String KEYWORD_AND = "And";
	private static final String KEYWORD_OR = "Or";
	private static final String KEYWORD_HAVING = "Having";
	private static final String KEYWORD_GROUP_BY = "Group"; // By
	private static final String KEYWORD_ORDER_BY = "Order"; // By
	private static final String KEYWORD_ORDER_ASC = "Asc";
	private static final String KEYWORD_ORDER_DESC = "Desc";

	private static final String[] KEYWORDS = new String[] { KEYWORD_BY,
			KEYWORD_GROUP_BY, KEYWORD_HAVING, KEYWORD_ORDER_BY };

	private static List<String> splitByWord(String name) {
		List<String> word = new ArrayList<String>();
		int length = name.length();
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < length; i++) {
			char c = name.charAt(i);
			if (Character.isUpperCase(c)) {
				word.add(sb.toString());
				sb.setLength(0); // clean
			}
			sb.append(c);
			if (i == length - 1) { // the last one
				word.add(sb.toString());
			}
		}
		return word;
	}

	private static List<String> splitByKeyword(String name) {
		List<String> result = new ArrayList<String>();
		List<String> words = splitByWord(name);
		int size = words.size();
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < size; i++) {
			String word = words.get(i);
			boolean isKeyword = false;
			if (Arrays.binarySearch(KEYWORDS, word) >= 0) {
				if ((KEYWORD_GROUP_BY.equals(word) || KEYWORD_ORDER_BY
						.equals(word)) && i < size - 1) {
					String nextWord = words.get(i + 1);
					if (KEYWORD_BY.equals(nextWord)) {
						isKeyword = true;
						word += nextWord;
						i++;
					}
				} else {
					isKeyword = true;
				}
			}
			if (isKeyword) {
				result.add(sb.toString());
				sb.setLength(0); // clean
			}
			sb.append(word);
			if (i == size - 1) { // the last one
				result.add(sb.toString());
			}
		}
		return result;

	}

	private static List<String> splitByKeywords(String text, String[] keywords) {
		List<String> result = new ArrayList<String>();
		List<String> words = splitByWord(text);
		int size = words.size();
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < size; i++) {
			String word = words.get(i);
			boolean isKeyword = false;
			if (Arrays.binarySearch(keywords, word) >= 0) {
				isKeyword = true;
			}
			if (isKeyword) {
				result.add(sb.toString());
				sb.setLength(0); // clean
			}
			sb.append(word);
			if (i == size - 1) { // the last one
				result.add(sb.toString());
			}
		}
		return result;
	}

	public static YoQuery parseDaoMethodToQuery(DaoMethod method) throws ProcessException {
		YoQuery query = null;
		if (method != null) {
			query = new YoQuery();
			String methodName = method.getMethodName();
			query.setName(methodName);

			String sql = method.getSql();
			if (sql != null) {
				query.setCrud(CRUD.RAW_SQL);
				query.setSql(sql);
			} else {
				List<String> keywords = splitByKeyword(methodName);
				if (keywords.size() > 0) {
					int argIndex = 0;
					for (int i = 0; i < keywords.size(); i++) {
						String word = keywords.get(i);
						if (i == 0) {
							CRUD crud = handleCrud(word);
							query.setCrud(crud);
						} else {
							argIndex = handleKeyword(query, word, argIndex);
						}
					}
				}
			}
		}
		if (query == null || query.getCrud() == null) {
			throw new ProcessException(method != null ? method.getElement() : null,
                    "Cann't support method name: " + method.getMethodName());
		}
		return query;
	}

	private static int handleKeyword(YoQuery query, String word, int argIndex) {
		if (word.startsWith(KEYWORD_BY)) {
			return handleWhere(query, word.substring(KEYWORD_BY.length()),
					argIndex);
		} else if (word.startsWith(KEYWORD_ORDER_BY)) {
			return handleOrderBy(query,
					word.substring((KEYWORD_ORDER_BY + KEYWORD_BY).length()),
					argIndex);
		} else if (word.startsWith(KEYWORD_GROUP_BY)) {
			return handleGroupBy(query,
					word.substring((KEYWORD_GROUP_BY + KEYWORD_BY).length()),
					argIndex);
		} else if (word.startsWith(KEYWORD_HAVING)) {
			return handleHaving(query, word.substring(KEYWORD_HAVING.length()),
					argIndex);
		}
		return argIndex;
	}

	private static int handleWhere(YoQuery query, String word, int argIndex) {
		String[] keywords = new String[] { KEYWORD_AND, KEYWORD_OR };
		List<String> words = splitByKeywords(word, keywords);
		if (words != null && words.size() > 0) {
			for (String q : words) {
				YoSelection selection = new YoSelection();
				if (q.startsWith(KEYWORD_AND)) {
					// and
					selection.setField(q.substring(KEYWORD_AND.length()));
					selection.setIsOr(false); // is and
				} else if (q.startsWith(KEYWORD_OR)) {
					// or
					selection.setField(q.substring(KEYWORD_OR.length()));
					selection.setIsOr(true); // is or
				} else {
					// and
					selection.setField(q);
					selection.setIsOr(false); // is and
				}
				selection.setArg(argIndex + "");
				argIndex++;
				query.selection(selection);
			}
		}
		return argIndex;
	}

	private static int handleOrderBy(YoQuery query, String word, int argIndex) {
		String[] keywords = new String[] { KEYWORD_AND };
		List<String> words = splitByKeywords(word, keywords);
		if (words != null && words.size() > 0) {
			for (String q : words) {
				if (q.startsWith(KEYWORD_AND)) {
					q = q.substring(KEYWORD_AND.length());
				}

				YoOrderBy order = new YoOrderBy();
				if (q.endsWith(KEYWORD_ORDER_ASC)) {
					q = q.substring(0, q.length() - KEYWORD_ORDER_ASC.length());
					order.setOrder(Order.ASC);
				} else if (q.endsWith(KEYWORD_ORDER_DESC)) {
					q = q.substring(0, q.length() - KEYWORD_ORDER_DESC.length());
					order.setOrder(Order.DESC);
				} else {
					order.setOrder(Order.ASC); // default
				}
				order.setField(q);
				query.orderBys(order);
			}
		}
		return argIndex;
	}

	private static int handleGroupBy(YoQuery query, String word, int argIndex) {
		String[] keywords = new String[] { KEYWORD_AND };
		List<String> words = splitByKeywords(word, keywords);
		if (words != null && words.size() > 0) {
			for (String q : words) {
				YoGroupBy groupBy = new YoGroupBy(q);
				query.groupBy(groupBy);
			}
		}
		return argIndex;
	}

	private static int handleHaving(YoQuery query, String word, int argIndex) {
		String[] keywords = new String[] { KEYWORD_AND };
		List<String> words = splitByKeywords(word, keywords);
		if (words != null && words.size() > 0) {
			for (String q : words) {
				YoHaving having = new YoHaving(q);
				query.having(having);
			}
		}
		return argIndex;
	}

	private static CRUD handleCrud(String methodName) {
		if (READ_PREFIX.equals(methodName)
				|| READ_ONE_PREFIX.equals(methodName)
				|| READ_LIST_PREFIX.equals(methodName)) {
			return CRUD.READ;
		}
		if (CREATE_PREFIX.equals(methodName)) {
			return CRUD.CREATE;
		}
		if (UPDATE_PREFIX.equals(methodName)) {
			return CRUD.UPDATE;
		}
		if (DELETE_PREFIX.equals(methodName)) {
			return CRUD.DELETE;
		}
		if (COUNT_PREFIX.equals(methodName)) {
			return CRUD.COUNT;
		}
		return null;
	}

	public static void main(String[] args) throws Exception {
		String methodName = "findOneByUsernamdeAndAge";
		/*-
		List<String> words = splitByWord(methodName);
		Assert.assertNotNull(words);
		Assert.assertEquals(6, words.size());
		Assert.assertEquals("find", words.get(0));
		Assert.assertEquals("One", words.get(1));
		Assert.assertEquals("By", words.get(2));
		Assert.assertEquals("Username", words.get(3));
		Assert.assertEquals("And", words.get(4));
		Assert.assertEquals("Age", words.get(5));
		System.out.println("words: " + words);

		methodName = "findOneByUsernameAndAgeOrAgeGroupByUsernameAndAgeHavingUsernameOrderByAgeAscAndUserNameDesc";
		// methodName = "deleteByUsernameAndAge";
		// methodName = "saveByUsernameAndAge";
		words = splitByKeyword(methodName);
		System.out.println("words: " + words);
		-*/

		methodName = "findOneByUsernameAndAgeOrAgeGroupByUsernameAndAgeHavingUsernameOrderByAgeAscAndUsernameDesc";
		DaoMethod method = new DaoMethod();
		method.setMethodName(methodName);
		DaoParam[] methodParams = new DaoParam[2];
		methodParams[0] = new DaoParam("username", String.class.getName());
		methodParams[1] = new DaoParam("age", int.class.getName());
		method.setMethodParams(methodParams);
		method.setReturnType("com.test.model.User");

		YoQuery query = parseDaoMethodToQuery(method);
		System.out.println("query: " + query);
		Assert.assertEquals(methodName, query.getName());
		Assert.assertEquals(CRUD.READ, query.getCrud());

		Assert.assertEquals(3, query.getSelections().size());
		Assert.assertEquals("Username", query.getSelections().get(0).getField());
		Assert.assertEquals("Age", query.getSelections().get(1).getField());
		Assert.assertEquals("Age", query.getSelections().get(2).getField());
		Assert.assertEquals(true, query.getSelections().get(2).isOr());

		Assert.assertEquals(1, query.getHavings().size());
		Assert.assertEquals("Username", query.getHavings().get(0).getField());

		Assert.assertEquals(2, query.getOrderBys().size());
		Assert.assertEquals("Age", query.getOrderBys().get(0).getField());
		Assert.assertEquals(Order.ASC, query.getOrderBys().get(0).getOrder());
		Assert.assertEquals("Username", query.getOrderBys().get(1).getField());
		Assert.assertEquals(Order.DESC, query.getOrderBys().get(1).getOrder());
	}

}