package com.palmnest.dao.hibernate;

import com.palmnest.application.bean.Page;
import com.palmnest.application.bean.Pagenation;
import com.palmnest.dao.GenericDao;
import com.palmnest.dao.SearchException;
import com.palmnest.dao.hibernate.support.BeanUtils;
import com.palmnest.model.Element;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.util.Version;
import org.hibernate.*;
import org.hibernate.criterion.*;
import org.hibernate.internal.CriteriaImpl;
import org.hibernate.search.FullTextSession;
import org.hibernate.search.Search;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.orm.ObjectRetrievalFailureException;
import org.springframework.orm.hibernate4.SessionFactoryUtils;
import org.springframework.util.Assert;

import javax.annotation.Resource;
import java.io.Serializable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This class serves as the Base class for all other DAOs - namely to hold
 * common CRUD methods that they might all use. You should only need to extend
 * this class when your require custom CRUD logic.
 * <p/>
 * <p>
 * To register this class in your Spring context file, use the following XML.
 *
 * <pre>
 *      &lt;bean id="fooDao" class="com.palmnest.dao.hibernate.GenericDaoHibernate"&gt;
 *          &lt;constructor-arg value="com.palmnest.model.Foo"/&gt;
 *      &lt;/bean&gt;
 * </pre>
 *
 * @param <T>  a type variable
 * @param <PK> the primary key for that type
 * @author <a href="mailto:chasel.java@gmail.com">Chasel Li</a>
 */
@Slf4j
public class GenericDaoHibernate<T, PK extends Serializable> implements
		GenericDao<T, PK> {
	/**
	 * Log variable for all child classes. Uses LogFactory.getLog(getClass())
	 * from Commons Logging
	 */

	private Class<T>       persistentClass;
	@Resource
	private SessionFactory sessionFactory;
	private Analyzer       defaultAnalyzer;

	/**
	 * Constructor that takes in a class to see which type of entity to persist.
	 * Use this constructor when subclassing.
	 *
	 * @param persistentClass the class type you'd like to persist
	 */
	public GenericDaoHibernate(final Class<T> persistentClass) {
		this.persistentClass = persistentClass;
		defaultAnalyzer = new StandardAnalyzer(Version.LUCENE_35);
	}

	/**
	 * Constructor that takes in a class and sessionFactory for easy creation of
	 * DAO.
	 *
	 * @param persistentClass the class type you'd like to persist
	 * @param sessionFactory  the pre-configured Hibernate SessionFactory
	 */
	public GenericDaoHibernate(final Class<T> persistentClass,
							   SessionFactory sessionFactory) {
		this.persistentClass = persistentClass;
		this.sessionFactory = sessionFactory;
		defaultAnalyzer = new StandardAnalyzer(Version.LUCENE_35);
	}

	public SessionFactory getSessionFactory() {
		return this.sessionFactory;
	}

	public Session getSession() throws HibernateException {
		Session sess = getSessionFactory().getCurrentSession();
		if (sess == null) {
			sess = getSessionFactory().openSession();
		}
		return sess;
	}

	@Autowired
	@Required
	public void setSessionFactory(SessionFactory sessionFactory) {
		this.sessionFactory = sessionFactory;
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public List<T> getAll() {
		Session sess = getSession();
		return sess.createCriteria(persistentClass).list();
	}

	/**
	 * {@inheritDoc}
	 */
	public List<T> getAllDistinct() {
		Collection<T> result = new LinkedHashSet<T>(getAll());
		return new ArrayList<T>(result);
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public List<T> search(String searchTerm) throws SearchException {
		Session         sess       = getSession();
		FullTextSession txtSession = Search.getFullTextSession(sess);

		org.apache.lucene.search.Query qry;
		try {
			qry = HibernateSearchTools.generateQuery(searchTerm,
					this.persistentClass, sess, defaultAnalyzer);
		} catch (ParseException ex) {
			throw new SearchException(ex);
		}
		org.hibernate.search.FullTextQuery hibQuery = txtSession
				.createFullTextQuery(qry, this.persistentClass);
		return hibQuery.list();
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public T get(PK id) {
		Session              sess   = getSession();
		IdentifierLoadAccess byId   = sess.byId(persistentClass);
		T                    entity = (T) byId.load(id);

		if (entity == null) {
			log.warn("Uh oh, '" + this.persistentClass + "' object with id '"
					+ id + "' not found...");
			throw new ObjectRetrievalFailureException(this.persistentClass, id);
		}

		return entity;
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public boolean exists(PK id) {
		Session              sess   = getSession();
		IdentifierLoadAccess byId   = sess.byId(persistentClass);
		T                    entity = (T) byId.load(id);
		return entity != null;
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public T save(T object) {
		Session sess = getSession();
		return (T) sess.merge(object);
	}

	@SuppressWarnings("unchecked")
	public T saveObj(T object) {
		Session sess = getSession();
		return (T) sess.save(object);
	}

	public void updateObj(T object) {
		Session sess = getSession();
		sess.update(object);
	}

	/**
	 * {@inheritDoc}
	 */
	public void remove(T object) {
		Session sess = getSession();
		sess.delete(object);
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public void remove(PK id) {
		Session              sess   = getSession();
		IdentifierLoadAccess byId   = sess.byId(persistentClass);
		T                    entity = (T) byId.load(id);
		sess.delete(entity);
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public List<T> findByNamedQuery(String queryName,
									Map<String, Object> queryParams) {
		Session sess       = getSession();
		Query   namedQuery = sess.getNamedQuery(queryName);

		for (String s : queryParams.keySet()) {
			namedQuery.setParameter(s, queryParams.get(s));
		}

		return namedQuery.list();
	}

	/**
	 * {@inheritDoc}
	 */
	public void reindex() {
		HibernateSearchTools.reindex(persistentClass, getSessionFactory()
				.getCurrentSession());
	}

	/**
	 * {@inheritDoc}
	 */
	public void reindexAll(boolean async) {
		HibernateSearchTools.reindexAll(async, getSessionFactory()
				.getCurrentSession());
	}

	/**
	 * 分页查询公共方法
	 *
	 * @param pageNo     页码
	 * @param pageSize   每页条数
	 * @param aliasMap   别名
	 * @param filterList 过滤集合
	 * @param orderList  排序集合
	 * @param clazz      查询实体对象
	 * @return
	 */
	public <T> Pagenation<T> queryPage(int pageNo, int pageSize,
									   Map<String, String> aliasMap, List<Criterion> filterList,
									   List<Order> orderList, Class<?> clazz) {
		Criteria criteria = createCriteria(aliasMap, filterList, orderList,
				clazz);
		Assert.notNull(criteria);
		Assert.isTrue(pageNo >= 1, "pageNo should start from 1");
		CriteriaImpl impl = (CriteriaImpl) criteria;

		// 先把Projection和OrderBy条件取出来,清空两者来执行Count操作
		Projection                    projection = impl.getProjection();
		List<CriteriaImpl.OrderEntry> orderEntries;
		try {
			orderEntries = (List) BeanUtils.forceGetProperty(impl,
					"orderEntries");
			BeanUtils.forceSetProperty(impl, "orderEntries", new ArrayList());
		} catch (Exception e) {
			throw new InternalError(" Runtime Exception impossibility throw ");
		}

		// 执行查询
		Object reObject = criteria.setProjection(Projections.rowCount())
				.uniqueResult();
		Long count      = (reObject == null ? 0 : (Long) reObject);
		int  totalCount = count.intValue();

		// 将之前的Projection和OrderBy条件重新设回去

		criteria.setProjection(projection);
		if (projection == null) {
			criteria.setResultTransformer(CriteriaSpecification.ROOT_ENTITY);
		}

		try {
			BeanUtils.forceSetProperty(impl, "orderEntries", orderEntries);
		} catch (Exception e) {
			throw new InternalError(" Runtime Exception impossibility throw ");
		}

		// 返回分页对象
		if (totalCount < 1)
			return new Pagenation<T>(new ArrayList<T>(), 1, 0);

		int startIndex = (pageNo - 1) * pageSize;
		criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY); // 设置ENTITY级的DISTINCT模式，根实体
		List<T> list = criteria.setFirstResult(startIndex)
				.setMaxResults(pageSize).list();
		return new Pagenation<T>(list, pageNo, totalCount, pageSize);
	}

	/**
	 * 分页查询公共方法
	 *
	 * @param pageNo     页码
	 * @param pageSize   每页条数
	 * @param aliasMap   别名
	 * @param filterList 过滤集合
	 * @param orderList  排序集合
	 * @param clazz      查询实体对象
	 * @return
	 */
	public <T> Pagenation<T> queryPage(Map<String, String> aliasMap, List<Criterion> filterList,
									   List<Order> orderList, Class<?> clazz) {
		Criteria criteria = createCriteria(aliasMap, filterList, orderList,
				clazz);
		Assert.notNull(criteria);
		//Assert.isTrue(pageNo >= 1, "pageNo should start from 1");
		CriteriaImpl impl = (CriteriaImpl) criteria;

		// 先把Projection和OrderBy条件取出来,清空两者来执行Count操作
		Projection                    projection = impl.getProjection();
		List<CriteriaImpl.OrderEntry> orderEntries;
		try {
			orderEntries = (List) BeanUtils.forceGetProperty(impl,
					"orderEntries");
			BeanUtils.forceSetProperty(impl, "orderEntries", new ArrayList());
		} catch (Exception e) {
			throw new InternalError(" Runtime Exception impossibility throw ");
		}

		// 执行查询
		Object reObject = criteria.setProjection(Projections.rowCount())
				.uniqueResult();
		Long count      = (reObject == null ? 0 : (Long) reObject);
		int  totalCount = count.intValue();

		// 将之前的Projection和OrderBy条件重新设回去

		criteria.setProjection(projection);
		if (projection == null) {
			criteria.setResultTransformer(CriteriaSpecification.ROOT_ENTITY);
		}

		try {
			BeanUtils.forceSetProperty(impl, "orderEntries", orderEntries);
		} catch (Exception e) {
			throw new InternalError(" Runtime Exception impossibility throw ");
		}

		// 返回分页对象
		if (totalCount < 1)
			return new Pagenation<T>(new ArrayList<T>(), 1, 0);

		//int startIndex = (pageNo - 1) * pageSize;
		criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY); // 设置ENTITY级的DISTINCT模式，根实体
		List<T> list = criteria.setFirstResult(0).setMaxResults(totalCount).list();//查询全部
		return new Pagenation<T>(list, 1, totalCount, totalCount);
	}

	public <T> Pagenation<T> queryPageByReceive(int pageNo, int pageSize,
												Map<String, String> aliasMap, List<Criterion> filterList,
												List<Order> orderList, Class<?> clazz) {
		Criteria criteria = createCriteria(aliasMap, filterList, orderList,
				clazz);
		Assert.notNull(criteria);
		Assert.isTrue(pageNo >= 1, "pageNo should start from 1");
		CriteriaImpl impl = (CriteriaImpl) criteria;

		// 先把Projection和OrderBy条件取出来,清空两者来执行Count操作
		Projection                    projection = impl.getProjection();
		List<CriteriaImpl.OrderEntry> orderEntries;
		try {
			orderEntries = (List) BeanUtils.forceGetProperty(impl,
					"orderEntries");
			BeanUtils.forceSetProperty(impl, "orderEntries", new ArrayList());
		} catch (Exception e) {
			throw new InternalError(" Runtime Exception impossibility throw ");
		}

		// 执行查询
		Object reObject = criteria.setProjection(Projections.rowCount())
				.uniqueResult();
		long temp       = (Long) reObject;
		int  totalCount = (reObject == null ? 0 : (int) temp);

		// 将之前的Projection和OrderBy条件重新设回去

		criteria.setProjection(projection);
		if (projection == null) {
			criteria.setResultTransformer(CriteriaSpecification.ROOT_ENTITY);
		}

		try {
			BeanUtils.forceSetProperty(impl, "orderEntries", orderEntries);
		} catch (Exception e) {
			throw new InternalError(" Runtime Exception impossibility throw ");
		}

		// 返回分页对象
		if (totalCount < 1)
			return new Pagenation<T>(new ArrayList<T>(), 1, 0);

		List<T> list = criteria.list();
		return new Pagenation<T>(list, pageNo, totalCount, pageSize);
	}

	private Criteria createCriteria(Map<String, String> aliasMap,
									List<Criterion> filterList, List<Order> orderList, Class<?> clazz) {
		Criteria criteria = getSession().createCriteria(clazz);
		for (String key : aliasMap.keySet()) {
			criteria.createAlias(key, aliasMap.get(key));
		}
		for (Criterion criterion : filterList) {
			criteria.add(criterion);
		}
		for (Order order : orderList) {
			criteria.addOrder(order);
		}
		return criteria;
	}

	/**
	 *20210224 lmz 不知道为什么是倒叙，目前注释了倒叙
	 */
//	@Override
//	public List<Element> getElementsByGroupName(String code) {
//
//		return getSession()
//				.createQuery(
//						"from Element el where el.isenabled='1' and el.elementGroup.groupcode = ? order by orderno desc")
//				.setString(0, code).list();
//	}
	
	
	@Override
	public List<Element> getElementsByGroupName(String code) {

		return getSession()
				.createQuery(
						"from Element el where el.isenabled='1' and el.elementGroup.groupcode = ? and el.elementGroup.isenabled=1 order by orderno ")
				.setString(0, code).list();
	}

	@Override
	public long getTabMaxOptVersion(String tableName) {
		String sql = "select max(oprateversion) from " + tableName;
		String result = getSession().createSQLQuery(sql).uniqueResult() == null ? ""
				: getSession().createSQLQuery(sql).uniqueResult() + "";
		if (result == null || "".equals(result)) {
			return 0;
		}
		return Long.valueOf(result);
	}

	@Override
	public JdbcTemplate getJdbcTemplate() {
		return new JdbcTemplate(SessionFactoryUtils.getDataSource(getSessionFactory()));
	}

	/**
	 * 分页查询函数，使用sql，并且同时使用JdbcTemplate来查询.<br/>
	 * Jdbctemplate是从getJdbcTemplate()中获取
	 * 如果传过来的页码大于等于Integer.MAX_VALUE，则表示查询全部，否则按照分页查询 如果页码=0，则表示只查询总数，不查询明细
	 * 现在只支持ORACLE数据库，如果其他数据库需要扩展
	 *
	 * @param pageNo 页号,从1开始.
	 * @author 0008841
	 */
	@Override
	public Page pagedQueryByJdbcTemplate(String sql, int pageNo, int pageSize) {
		if (StringUtils.isEmpty(sql)) {
			return new Page<T, PK>();
		}
		if (pageNo == 0) {
			pageNo = 1;
		}

		// Count查询
		// 如果传过来的页码大于等于Integer.MAX_VALUE，则表示查询全部，否则按照分页查询:即页码小于的时候查询总数
		int totalCount = 0;
		if (pageSize < Integer.MAX_VALUE) {
			String countQueryString = "";
			//if (sql.toLowerCase().indexOf("union") > 0) {// union 语句
			countQueryString = " select count(*) num from (" + sql
					+ ") tabs";
			//} else {
			//	countQueryString = " select count(*) num " + removeSelect(removeOrders(sql));
			//}
			totalCount = getJdbcTemplate().queryForInt(countQueryString);
			if (totalCount < 1) {
				return new Page<T, PK>();
			}

			if (pageSize == 0) {
				return new Page<T, PK>(1, totalCount, pageSize,
						new ArrayList<T>());
			}
		}
		List list = null;
		// 如果页大小为0，则查询全部
		if (pageSize == 0) {
			list = getJdbcTemplate().queryForList(sql);
		} else {
			// 获取分页查询的SQL,现在只支持mysql数据库，如果其他数据库需要扩展
			sql = this.getMySqlPageSql(sql, pageNo, pageSize);
			list = getJdbcTemplate().queryForList(sql);
		}
		//int startIndex = Page.getStartOfPage(pageNo, pageSize);
		return new Page<T, PK>(pageNo, totalCount, pageSize, list);
	}

	public String getMySqlPageSql(String sql, int pageNo, int pageSize) {
		StringBuilder sb     = new StringBuilder(sql);
		int           offset = (pageNo - 1) * pageSize;
		sb.append(" limit " + offset + "," + pageSize);
		return sb.toString();
	}

	public String getOraclePageSql(String sql, int pageNo, int pageSize) {
		StringBuilder sb = new StringBuilder(sql);
		// 计算第一条记录的位置，Oracle分页是通过rownum进行的，而rownum是从1开始的
		int offset = (pageNo - 1) * pageSize + 1;
		sb.insert(0, "select u.*, rownum r from (").append(") u ");
		sb.insert(0, "select * from (").append(") where r >= ").append(offset)
				.append(" and r < ").append(offset + pageSize);
		// 上面的Sql语句拼接之后大概是这个样子：
		// select * from (select u.*, rownum r from (select * from t_user) u
		// where rownum < 31)
		// where r >= 16
		return sb.toString();
	}

	/**
	 * 去除hql的orderby 子句，用于pagedQuery.
	 */
	private static String removeOrders(String hql) {
		Assert.hasText(hql);
		Pattern p = Pattern.compile("order\\s*by[\\w|\\W|\\s|\\S]*",
				Pattern.CASE_INSENSITIVE);
		Matcher      m  = p.matcher(hql);
		StringBuffer sb = new StringBuffer();
		while (m.find()) {
			m.appendReplacement(sb, "");
		}
		m.appendTail(sb);
		return sb.toString();
	}

	/**
	 * 去除hql的select 子句，未考虑union的情况,用于pagedQuery.
	 */
	private static String removeSelect(String hql) {
		Assert.hasText(hql);
		int beginPos = hql.toLowerCase().indexOf("from");
		Assert.isTrue(beginPos != -1, " hql : " + hql
				+ " must has a keyword 'from'");
		return hql.substring(beginPos);
	}
}
