package com.uas.search.console.b2b.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.uas.search.b2b.exception.SearchException;
import com.uas.search.b2b.model.MultiValue;
import com.uas.search.b2b.model.PageParams;
import com.uas.search.b2b.model.SPage;
import com.uas.search.b2b.service.SearchService;
import com.uas.search.console.b2b.service.InnerSearchService;
import com.uas.search.console.b2b.util.ClassAndTableNameUtils;
import com.uas.search.console.b2b.util.DocumentToObjectUtils;
import com.uas.search.console.b2b.util.FileUtils;
import com.uas.search.console.b2b.util.SearchUtils;
import com.uas.search.util.ArrayUtils;
import org.apache.lucene.document.Document;
import org.apache.lucene.search.*;
import org.apache.lucene.search.BooleanClause.Occur;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import com.uas.search.util.CollectionUtils;
import com.uas.search.util.StringUtils;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;
import java.util.Map.Entry;

/**
 * 搜索实现
 * 
 * @author sunyj
 * @since 2016年11月9日 下午4:47:09
 */
@Service
public class SearchServiceImpl implements SearchService, InnerSearchService {

	/**
	 * 默认的页码
	 */
	private static final int PAGE_INDEX = 1;

	/**
	 * 默认每页的大小
	 */
	private static final int PAGE_SIZE = 20;

	private Logger logger = LoggerFactory.getLogger(getClass());

	@Override
	public SPage<Long> searchIds(String keyword, Table_name tableName, PageParams pageParams) throws SearchException {
		IndexSearcher indexSearcher = SearchUtils.getIndexSearcher(tableName);
		// 获取单据的id
		List<Long> content = new ArrayList<>();
		try {
			// 获取该表keyword可以搜索的域
			List<String> keywordFields = null;
			// 若未指定，再获取实体类中所配置的
			if (pageParams != null && !CollectionUtils.isEmpty(pageParams.getKeywordSearchColumns())) {
				keywordFields = getKeywordFields(tableName, pageParams.getKeywordSearchColumns());
			} else {
				keywordFields = ClassAndTableNameUtils.getKeywordFields(tableName);
			}
			SPage<ScoreDoc> scoreDocPage = search(indexSearcher, keyword, tableName, keywordFields, true, pageParams);
			SPage<Long> sPage = convertSPage(scoreDocPage, Long.class);
			for (ScoreDoc scoreDoc : scoreDocPage.getContent()) {
				Document document = indexSearcher.doc(scoreDoc.doc);
				content.add(Long.valueOf(document.get(ClassAndTableNameUtils.getIdField(tableName))));
			}
			sPage.setContent(content);
			return sPage;
		} catch (NumberFormatException | IOException e) {
			throw new SearchException(e).setDetailedMessage(e);
		} finally {
			SearchUtils.releaseIndexSearcher(indexSearcher);
		}
	}

	/**
	 * 获取指定的可以搜索的字段列名
	 * 
	 * @param tableName
	 *            单据类型
	 * @param keywordSearchColumns
	 *            keyword模糊搜索的字段名称
	 * @return 可以搜索的字段列名
	 */
	private List<String> getKeywordFields(Table_name tableName, List<String> keywordSearchColumns) {
		List<String> result = new ArrayList<>();
		for (String keywordSearchColumn : keywordSearchColumns) {
			// 返回之前先拼接索引域名
			result.add(ClassAndTableNameUtils.combineField(tableName, keywordSearchColumn));
		}
		return result;
	}

	@Override
	public SPage<String> similar(String keyword, Table_name tableName, Integer size, String... fields)
			throws SearchException, IllegalArgumentException {
		if (ArrayUtils.isEmpty(fields)) {
			throw new IllegalArgumentException("fields不可为空");
		}
		if (keyword == null) {
			keyword = "";
		}

		PageParams pageParams = new PageParams();
		if (size == null || size < 1) {
			size = PAGE_SIZE;
		}
		pageParams.setPage(PAGE_INDEX);
		pageParams.setSize(size);

		IndexSearcher indexSearcher = SearchUtils.getIndexSearcher(tableName);
		List<String> content = new ArrayList<>();
		try {
			List<String> keywordFields = new ArrayList<>();
			for (String field : fields) {
				keywordFields.add(ClassAndTableNameUtils.combineField(tableName, field));
			}
			// 获取联想词时，不进行分词（此时关键词需转为小写），但是中文很不准确，所以还是进行分词
			SPage<ScoreDoc> scoreDocPage = search(indexSearcher, keyword, tableName, keywordFields, true, pageParams);
			SPage<String> sPage = convertSPage(scoreDocPage, String.class);
			for (ScoreDoc scoreDoc : scoreDocPage.getContent()) {
				Document document = indexSearcher.doc(scoreDoc.doc);
				for (String field : keywordFields) {
					String value = document.get(field);
					// 需包含关键词，才作为联想词（关键词视为一个完整的字符串，中间不能掺有其他字符）
					if (value != null && value.toUpperCase().contains(keyword.toUpperCase())) {
						content.add(value);
						break;
					}
				}
			}
			sPage.setContent(content);
			return sPage;
		} catch (IOException e) {
			throw new SearchException(e).setDetailedMessage(e);
		} finally {
			SearchUtils.releaseIndexSearcher(indexSearcher);
		}
	}

	/**
	 * 转换SPage
	 * 
	 * @param scoreDocPage
	 * @param clazz
	 * @return
	 */
	private <T> SPage<T> convertSPage(SPage<ScoreDoc> scoreDocPage, Class<T> clazz) {
		return new SPage<>(scoreDocPage.getTotalPage(), scoreDocPage.getTotalElement(), scoreDocPage.getPage(),
                scoreDocPage.getSize(), scoreDocPage.isFirst(), scoreDocPage.isLast());
	}

	/**
	 * 根据关键词、单据类型、状态码搜索单据
	 * 
	 * @param indexSearcher
	 * 
	 * @param keyword
	 *            不为空，模糊搜索关键词，可以是：单据编号、供应商uu（采购）、供应商名称（采购）、客户uu（销售）、客户名称（销售）、
	 *            物料编号、 物料名称、物料规格
	 * @param tableName
	 *            不为空，单据类型
	 * @param keywordFields
	 *            不为空，模糊搜索的域，多个域之间取或操作
	 * @param tokenized
	 *            可为空，模糊搜索时，是否对关键词进行分词，为空时，不分词
	 * @param pageParams
	 *            可为空，翻页、过滤、排序、排除过滤等信息
	 *            <p>
	 *            关于过滤，通过键值对指定过滤条件，值的类型由键决定：
	 *            </p>
	 * 
	 *            <table border=1 cellpadding=5 cellspacing=0 summary=
	 *            "Fields and types">
	 *            <tr>
	 *            <th>Description</th>
	 *            <th>Field</th>
	 *            <th>Type</th>
	 *            </tr>
	 *            <tr>
	 *            <td>状态、所属企业uu、其他状态（如已采纳、未采纳等）</td>
	 *            <td>键为数据库表中相应的字段名称</td>
	 *            <td>值为字段对应的值，若值有多个，则使用
	 *            {@link com.uas.search.b2b.model.MultiValue}</td>
	 *            </tr>
	 *            <tr>
	 *            <td>开始时间、截止时间， 这两个参数用于对时间范围进行筛选</td>
	 *            <td>{@link com.uas.search.b2b.util.SearchConstants}</td>
	 *            <td>Long</td>
	 *            </tr>
	 *            <tr>
	 *            <td>排序方式</td>
	 *            <td>{@link com.uas.search.b2b.util.SearchConstants}</td>
	 *            <td>List({@link com.uas.search.b2b.model.Sort})</td>
	 *            </tr>
	 *            </table>
	 * 
	 *            <p>
	 *            关于排除过滤，键为数据库表中相应的字段名称，值为需排除的值，若值有多个，则使用
	 *            {@link com.uas.search.b2b.model.MultiValue}
	 *            </p>
	 * @return 单据数据
	 * @throws IOException
	 */
	@SuppressWarnings("unchecked")
	private SPage<ScoreDoc> search(IndexSearcher indexSearcher, String keyword, Table_name tableName,
			List<String> keywordFields, Boolean tokenized, PageParams pageParams) throws IOException {

		if (CollectionUtils.isEmpty(keywordFields)) {
			throw new IllegalArgumentException("keywordFields不可为空");
		}

		SPage<ScoreDoc> sPage = new SPage<>();
		BooleanQuery booleanQuery = new BooleanQuery();
		Sort sort = null;

		if (keyword == null) {
			keyword = "";
		}
		// 关键词无效，即搜索所有的数据
		// 关键词带空格，进行与操作
		String[] strs = keyword.split(" ");
		for (String str : strs) {
			// keyword可能是哪些域，域之间进行或操作
			BooleanQuery booleanQuery2 = new BooleanQuery();
			for (String keywordField : keywordFields) {
				booleanQuery2.add(SearchUtils.regexpQuery(keywordField, str, tokenized), BooleanClause.Occur.SHOULD);
			}
			booleanQuery.add(booleanQuery2, BooleanClause.Occur.MUST);
		}

		// 分页信息
		if (pageParams == null) {
			sPage.setPage(PAGE_INDEX);
			sPage.setSize(PAGE_SIZE);
			sPage.setFirst(true);
		} else {
			int page = pageParams.getPage();
			if (page > 0) {
				sPage.setPage(page);
			} else {
				sPage.setPage(PAGE_INDEX);
				sPage.setFirst(true);
			}
			int size = pageParams.getSize();
			if (size > 0) {
				sPage.setSize(size);
			} else {
				sPage.setSize(PAGE_SIZE);
			}

			Map<String, Object> filters = pageParams.getFilters();
			if (!CollectionUtils.isEmpty(filters)) {
				// 如果filter中开始时间和截止时间至少传了一个参数，说明需要对时间范围进行限定
				if (!StringUtils.isEmpty(filters.get(com.uas.search.b2b.util.SearchConstants.FROM_DATE_KEY))
						|| !StringUtils.isEmpty(filters.get(com.uas.search.b2b.util.SearchConstants.END_DATE_KEY))) {
					// 开始时间初始化为1990.1.1
					Calendar calendar = Calendar.getInstance();
					calendar.set(1990, 0, 1);
					// 截止时间初始化为现在
					long fromTime = calendar.getTimeInMillis();
					long endTime = new Date().getTime();
					if (!StringUtils.isEmpty(filters.get(com.uas.search.b2b.util.SearchConstants.FROM_DATE_KEY))) {
						fromTime = Long.parseLong(
								filters.remove(com.uas.search.b2b.util.SearchConstants.FROM_DATE_KEY).toString());
					}
					if (!StringUtils.isEmpty(filters.get(com.uas.search.b2b.util.SearchConstants.END_DATE_KEY))) {
						endTime = Long.parseLong(
								filters.remove(com.uas.search.b2b.util.SearchConstants.END_DATE_KEY).toString());
					}
					booleanQuery.add(NumericRangeQuery.newLongRange(ClassAndTableNameUtils.getDateField(tableName),
							fromTime, endTime, true, true), BooleanClause.Occur.MUST);
				}

				// 如果需要排序
				if (!StringUtils.isEmpty(filters.get(com.uas.search.b2b.util.SearchConstants.SORT_KEY))) {
					Object sortObject = filters.remove(com.uas.search.b2b.util.SearchConstants.SORT_KEY);
					List<com.uas.search.b2b.model.Sort> sortList = null;
					if (sortObject instanceof ArrayList) {
						sortList = (List<com.uas.search.b2b.model.Sort>) sortObject;
					}
					if (!CollectionUtils.isEmpty(sortList)) {
						SortField[] sortFields = new SortField[sortList.size()];
						for (int i = 0; i < sortList.size(); i++) {
							com.uas.search.b2b.model.Sort s = sortList.get(i);
							SortField sortField = new SortField(
									ClassAndTableNameUtils.combineField(tableName, s.getField()), getType(s),
									!s.isReverse());
							if (s.getMissingValue() != null) {
								sortField.setMissingValue(s.getMissingValue());
							}
							sortFields[i] = sortField;
						}
						sort = new Sort(sortFields);
					}
				}

				// 其他过滤条件，键即为相应的数据库字段名（field名）
				Set<Entry<String, Object>> entrySet = filters.entrySet();
				for (Entry<String, Object> entry : entrySet) {
					String key = entry.getKey();
					Object value = entry.getValue();
					// 拼接索引域名
					String field = ClassAndTableNameUtils.combineField(tableName, key);
					/*
					 * 对于明细、企业等所关联的表的数据，是以json格式存储的，搜索时可能会对明细等表中多个字段进行过滤，
					 * 但是键都是明细等表在主表中所对应的字段名称，为了实现这样的过滤功能，需要将多个值放在com.uas.search.
					 * b2b.model.MultiValue中， 作为filters中key-value的value传递过来
					 */
					if (value instanceof MultiValue) {
						MultiValue multiValue = (MultiValue) value;
						List<Object> values = multiValue.getValues();
						Occur occur = multiValue.isOr() ? Occur.SHOULD : Occur.MUST;
						BooleanQuery booleanQuery2 = new BooleanQuery();
						for (Object object : values) {
							booleanQuery2.add(SearchUtils.regexpQuery(field, String.valueOf(object), false), occur);
						}
						booleanQuery.add(booleanQuery2, Occur.MUST);
					} else {
						if (value != null) {
							booleanQuery.add(SearchUtils.regexpQuery(field, String.valueOf(value), false), Occur.MUST);
						}
					}

				}
			}

			// 排除过滤
			Map<String, Object> notEqualFilters = pageParams.getNotEqualFilters();
			if (!CollectionUtils.isEmpty(notEqualFilters)) {
				Set<Entry<String, Object>> entrySet = notEqualFilters.entrySet();
				for (Entry<String, Object> entry : entrySet) {
					String key = entry.getKey();
					Object value = entry.getValue();
					String field = ClassAndTableNameUtils.combineField(tableName, key);
					// 排除多个值
					if (value instanceof MultiValue) {
						MultiValue multiValue = (MultiValue) value;
						List<Object> values = multiValue.getValues();
						Occur occur = multiValue.isOr() ? Occur.SHOULD : Occur.MUST;
						BooleanQuery booleanQuery2 = new BooleanQuery();
						for (Object object : values) {
							booleanQuery2.add(SearchUtils.regexpQuery(field, String.valueOf(object), false), occur);
						}
						booleanQuery.add(booleanQuery2, Occur.MUST_NOT);
					} else {
						if (value != null) {
							booleanQuery.add(SearchUtils.regexpQuery(field, String.valueOf(value), false),
									Occur.MUST_NOT);
						}
					}
				}
			}
		}
		logger.info(booleanQuery.toString());

		TopDocs topDocs;
		// 如果页码不为1
		if (sPage.getPage() > 1) {
			TopDocs previousTopDocs = null;
			if (sort != null) {
				previousTopDocs = indexSearcher.search(booleanQuery, (sPage.getPage() - 1) * sPage.getSize(), sort);
			} else {
				previousTopDocs = indexSearcher.search(booleanQuery, (sPage.getPage() - 1) * sPage.getSize());
			}
			int totalHits = previousTopDocs.totalHits;
			ScoreDoc[] previousScoreDocs = previousTopDocs.scoreDocs;
			if ((sPage.getPage() - 1) * sPage.getSize() >= totalHits) {
				throw new SearchException("页码过大：元素总数量为" + totalHits);
			}
			if (sort != null) {
				topDocs = indexSearcher.searchAfter(previousScoreDocs[previousScoreDocs.length - 1], booleanQuery,
						sPage.getSize(), sort);
			} else {
				topDocs = indexSearcher.searchAfter(previousScoreDocs[previousScoreDocs.length - 1], booleanQuery,
						sPage.getSize());
			}
		} else {
			sPage.setFirst(true);
			if (sort != null) {
				topDocs = indexSearcher.search(booleanQuery, sPage.getSize(), sort);
			} else {
				topDocs = indexSearcher.search(booleanQuery, sPage.getSize());
			}
		}

		int totalHits = topDocs.totalHits;
		// 设置总元素个数、页数等信息
		sPage.setTotalElement(totalHits);
		int totalPage = (int) Math.ceil(totalHits / (1.0 * sPage.getSize()));
		sPage.setTotalPage(totalPage);
		if (totalPage == sPage.getPage()) {
			sPage.setLast(true);
		}

		sPage.setContent(Arrays.asList(topDocs.scoreDocs));
		return sPage;
	}

	@Override
	public <T> T getObjectById(Long id, Class<T> clazz) {
		if (id == null) {
			return null;
		}
		try {
			Document document = SearchUtils.getDocumentById(ClassAndTableNameUtils.toTableName(clazz),
					ClassAndTableNameUtils.getIdField(clazz), id);
			T element = DocumentToObjectUtils.toObject(document, clazz);
			return element;
		} catch (SearchException e) {
			logger.error("", e);
			return null;
		}
	}

	@Override
	public <T> SPage<T> getAllObjects(Class<T> clazz, int page, int size) {
		Table_name tableName = ClassAndTableNameUtils.toTableName(clazz);
		IndexSearcher indexSearcher = SearchUtils.getIndexSearcher(tableName);
		// 获取单据的id
		List<T> content = new ArrayList<>();
		try {
			PageParams pageParams = new PageParams();
			pageParams.setPage(page);
			pageParams.setSize(size);
			// 获取该表keyword可以搜索的域
			List<String> keywordFields = ClassAndTableNameUtils.getKeywordFields(tableName);
			SPage<ScoreDoc> scoreDocPage = search(indexSearcher, null, tableName, keywordFields, null, pageParams);
			SPage<T> sPage = convertSPage(scoreDocPage, clazz);
			for (ScoreDoc scoreDoc : scoreDocPage.getContent()) {
				Document document = indexSearcher.doc(scoreDoc.doc);
				content.add(DocumentToObjectUtils.toObject(document, clazz));
			}
			sPage.setContent(content);
			return sPage;
		} catch (NumberFormatException | IOException e) {
			throw new SearchException(e).setDetailedMessage(e);
		} finally {
			SearchUtils.releaseIndexSearcher(indexSearcher);
		}
	}

	/**
	 * 获取排序字段的类型
	 * 
	 * @param s
	 * @return
	 */
	private SortField.Type getType(com.uas.search.b2b.model.Sort s) {
		if (s == null) {
			return null;
		}
		com.uas.search.b2b.model.Sort.Type type = s.getType();
		switch (type) {
		case STRING:
			return SortField.Type.STRING;
		case INT:
			return SortField.Type.INT;
		case FLOAT:
			return SortField.Type.FLOAT;
		case LONG:
			return SortField.Type.LONG;
		case DOUBLE:
			return SortField.Type.DOUBLE;
		default:
			throw new SearchException("排序参数错误：" + type);
		}
	}

	@Override
	public <T> String writeObjectsToFiles(Class<T> clazz) {
		// 不能边更新索引边分页获取索引中的数据，因为索引更新后，分页顺序可能也会变化，
		// 所以要先把数据保存到本地，等待全部获取之后重建索引
		Long startTime = new Date().getTime();
		Table_name tableName = ClassAndTableNameUtils.toTableName(clazz);
		// 先删除旧的文件
		FileUtils.deleteSubFiles(new File(SearchUtils.getDataPath(tableName)));

		int page = PAGE_INDEX;
		int size = 1000;
		SPage<T> sPage = getAllObjects(clazz, page, size);

		// 索引中数据的总数目
		long totalElements = sPage.getTotalElement();
		logger.info("发现数据：" + totalElements + "条");
		int fileIndex = 1;
		PrintWriter printWriter = null;
		int count = 0;
		try {
			File file = new File(SearchUtils.getDataPath(tableName));
			if (!file.exists()) {
				file.mkdirs();
			}
			printWriter = new PrintWriter(SearchUtils.getDataPath(tableName) + "/" + fileIndex + ".txt");
			while (true) {
				// 一个文件存放100000条数据，一旦超过，写入新的文件
				if (count > IndexServiceImpl.SINGLE_FILE_MAX_SIZE) {
					count = 1;
					printWriter.flush();
					printWriter.close();
					fileIndex++;
					printWriter = new PrintWriter(SearchUtils.getDataPath(tableName) + "/" + fileIndex + ".txt");
				}
				List<T> content = sPage.getContent();
				for (T element : content) {
					printWriter.println(JSONObject.toJSONString(element));
					count++;
				}
				logger.info(String.format(tableName.value() + "...................%.2f%%",
						((page - 1) * size + content.size()) * 100.0 / totalElements));

				if (++page > sPage.getTotalPage()) {
					break;
				}
				sPage = getAllObjects(clazz, page, size);
			}
			printWriter.flush();
			printWriter.close();
		} catch (FileNotFoundException | SecurityException | IllegalArgumentException e) {
			throw new SearchException(e).setDetailedMessage(e);
		}
		Long endTime = new Date().getTime();
		String message = String.format("写入数据%s条，耗时%.2fs\n ", totalElements, (endTime - startTime) / 1000.0);
		logger.info(message);
		return message;
	}
}
