/*
 * LOGICAL-PARADOX.ORG
 * Copyright (C)2005 satoshi akabane(akabane@logical-paradox.org)
 * $Id: RSSSearchEngine.java,v 1.11 2006/02/27 14:20:33 akabane Exp $
 */
package org.logical_paradox.koike.rss.search;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.ResourceBundle;

import org.logical_paradox.common.util.StringUtils;
import org.logical_paradox.koike.core.KoikeConfig;
import org.logical_paradox.koike.core.KoikeConstant;
import org.logical_paradox.koike.core.document.Document;
import org.logical_paradox.koike.core.indexer.IndexAccessException;
import org.logical_paradox.koike.core.indexer.IndexNotFoundException;
import org.logical_paradox.koike.core.parser.ParserException;
import org.logical_paradox.koike.core.search.Element;
import org.logical_paradox.koike.core.search.InvertedIndex;
import org.logical_paradox.koike.core.search.KoikeIndexResultSet;
import org.logical_paradox.koike.core.search.ResultSet;
import org.logical_paradox.koike.core.search.SearchCondition;
import org.logical_paradox.koike.core.search.SearchEngine;
import org.logical_paradox.koike.core.search.SearchException;
import org.logical_paradox.koike.core.search.SearchResult;
import org.logical_paradox.koike.core.summary.DefaultSummaryGenerator;
import org.logical_paradox.koike.core.summary.SummaryGenerator;
import org.logical_paradox.koike.rss.document.ContentsAttribute;
import org.logical_paradox.koike.rss.search.lo.LogicalOperationException;
import org.logical_paradox.koike.rss.search.lo.LogicalOperatorFactory;

/**
 * RSS/1b用全文検索エンジン
 * @author satoshi akabane@logical-paradox.org
 * @version $Revision: 1.11 $
 */
public class RSSSearchEngine extends SearchEngine {
	/** サマリージェネレータ */
	private static final SummaryGenerator summaryGenerator = new DefaultSummaryGenerator();
	/** リソース名 */
	private static final String DEFAULT_RESOURCE_PFX = "rss_search_engine";

	/** リソース */
	private final ResourceBundle resource;
	/** 検索結果ソートアルゴリズム */
	private final Comparator sortingAlgorithm;

	/**
	 * コンストラクタ
	 * @param cfg コンフィグレーション
	 * @throws Exception インスタンスの生成に失敗した場合に発生する例外
	 */
	public RSSSearchEngine(KoikeConfig cfg) throws Exception {
		super(cfg);
		resource = ResourceBundle.getBundle(DEFAULT_RESOURCE_PFX);

		// ソートメソッドのロード
		sortingAlgorithm = (Comparator)Class.forName(resource.getString("sort.method.classname")).newInstance();
	}
	/**
	 * 検索処理を実行する
	 * @param condition 検索条件
	 * @return 結果セット
	 * @throws SearchException 検索処理中に発生した障害
	 */
	public SearchResult search(SearchCondition condition) throws SearchException {
		// 検索語を探す
		ArrayList<String> searchwords = new ArrayList<String>();
		KoikeIndexResultSet indexes = retrieve(searchwords, condition);
		if(indexes == null || indexes.isEmpty() == true) {
			// 検索条件に合致するものについて、インデックスシステムに問い合わせた結果、
			// 回答がなかった場合はnull
			return new SearchResult();
		}

		return fetchResults(indexes, condition, searchwords.toArray(new String[0]));
	}
	/**
	 * インデックスシステムからの回答をもとにして、サーチエンジンとしての検索結果を返す
	 * @param indexes インデックスシステムからの回答
	 * @param condition 検索条件
	 * @return 検索結果(1ページ分)
	 */
	protected SearchResult fetchResults(KoikeIndexResultSet indexes, SearchCondition condition, String[] searchwords) throws SearchException { 
		int doccnt = 0;
		int hits = 0;
		int skipcnt = condition.getStartp();

		ArrayList<ResultSet> results = new ArrayList<ResultSet>();

		for(Iterator it = indexes.iteratorInvertedIndexes(); it.hasNext();) {
			InvertedIndex iidx = (InvertedIndex)it.next();
			String nodeId = iidx.getNodeId();
			int blkno = iidx.getBlockNo();
			long documents = iidx.getDocumentAvailability();

			int docno = blkno * KoikeConstant.INVERTED_INDEX_BLK_SIZE;

			// 転置インデックスに含まれる文書数を計測する
			while(documents > 0) {
				if((documents & 1) != 0) {
					hits++;
				}
				documents >>>= 1;
			}
			documents = iidx.getDocumentAvailability();
			// 全てのビットを確認する
			while((doccnt < condition.getDispnum() || condition.getDispnum() == 0) && documents != 0) {
				if((documents & 1) != 0) {
					if(skipcnt > 0) {
						skipcnt--;
					} else {
						doccnt++;
						results.add(createResultSet(nodeId, docno, searchwords));
					}
				}
				documents >>>= 1;
				docno++;
			}
		}

		Comparator c = getComparator();
		if(c != null) {
			// ソート方法が明示的に指定されている場合，そのルールで検索結果をソートする
			Collections.sort(results, c);
		}

		// 検索結果を作成する
		SearchResult sr = new SearchResult();
		sr.setResultSet((ResultSet[])results.toArray(new ResultSet[0]));
		sr.setHits(hits);

		return sr;
	}
	/**
	 * 検索システムとしての検索結果を生成する
	 * @param nodeId ノードID
	 * @param docno 文書番号
	 * @return 検索結果
	 * @throws SearchException	検索結果の生成に失敗した
	 */
	protected ResultSet createResultSet(String nodeId, int docno, String[] terms) throws SearchException {
		try {
			// ドキュメント管理システムに問い合わせて、ドキュメントそのものを取得する
			String digest = "";
			if(StringUtils.isEmpty(nodeId)) {
				digest = "" + docno;
			} else {
				digest = nodeId + ":" + docno;
			}
			Document document = getDocumentAccessor().getDocument(digest);
			// サマリーを作成する
			String summary = summaryGenerator.createSummary(document.getDocument(), terms);

			ContentsAttribute attribute = (ContentsAttribute)document.getAttribute();

			// 検索結果を生成する
			ResultSet rs = new ResultSet();
			rs.setNodeId(nodeId);
			rs.setDocno(docno);
			rs.setDocumentTitle(attribute.getTitle());
			rs.setSiteName(attribute.getSitename());
			rs.setUrl(attribute.getUrl());
			rs.setSummary(summary);

			return rs;
		} catch(SearchException e) {
			throw e;
		} catch(Exception e) {
			throw new SearchException(e);
		}
	}
	/**
	 * 指定要素について，インデックスを検索して結果を得る
	 * 指定要素に更に子の要素がある場合，再帰的に処理される
	 * @param searchwords 検出された検索語を登録するリスト
	 * @param element 要素
	 * @return 結果セット
	 * @throws SearchException 検索処理中に発生した例外
	 */
	protected KoikeIndexResultSet retrieve(List<String> searchwords, Element element) throws SearchException {
		if(element.getElementType() == Element.TYPE_TERM) {
			// 検出された検索語を登録する
			searchwords.add(element.getAttribute());
			// インデックスシステムに対して問い合わせる
			return executeQuery(element.getAttribute());
		}
		// まず最初に，子要素の全ての検索結果を取りまとめる
		List childElements = element.getChildNodes();
		KoikeIndexResultSet[] childResults = new KoikeIndexResultSet[childElements.size()];

		for(int i = 0; i < childResults.length; i++) {
			childResults[i] = retrieve(searchwords, (Element)childElements.get(i));
			if(childResults[i] == null) {
				return null;
			}
		}

		// 子要素の結果を受け取った後で，この要素で指定されている論理演算を適用する
		try {
			KoikeIndexResultSet[] results = LogicalOperatorFactory.getOperator(element.getElementType()).execute(childResults);
			// 結果を取りまとめる
			// この場合も，第1検索語が演算結果となる
			return results == null || results.length == 0 ? null : results[0];
		} catch (LogicalOperationException e) {
			throw new SearchException(e);
		}
	}
	/**
	 * インデックスシステムに対して問い合わせを投げる
	 * @param keyword 検索語
	 * @return 結果セット
	 */
	protected KoikeIndexResultSet executeQuery(String keyword) throws SearchException {
		try {
			// まず，検索語を索引語に変換する
			// つまり，n-gramに分割する
			String[] terms = getParser().split(keyword);
			KoikeIndexResultSet[] results = new KoikeIndexResultSet[terms.length];
			if(results == null || results.length == 0) {
				return null;
			}
			for(int i = 0; i < terms.length; i++) {
				results[i] = getIndexAccessor().getValue(terms[i]);
				if(results[i] == null) {
					// インデックスシステムに、該当する索引語がそもそも登録されていなかったので結果なし
					return null;
				}

				if(i > 0) {
					// 第1索引語以外の場合，第1索引語と転置インデックスのANDをとる
					KoikeIndexResultSet[] andOperationApplied = LogicalOperatorFactory.getOperator(Element.TYPE_AND).execute(new KoikeIndexResultSet[]{results[0], results[i]});
					if(andOperationApplied == null) {
						// ANDをとった結果，何も残らなかった
						return null;
					}
					results[0] = andOperationApplied[0];
					results[i] = andOperationApplied[1];
				}
			}
			// 最後に，転置インデックスのSEQをとる
			KoikeIndexResultSet[] seqOperationApplied = LogicalOperatorFactory.getOperator(Element.TYPE_SEQ).execute(results);
			if(seqOperationApplied == null) {
				// SEQをとった結果，何も残らなかった
				return null;
			}

			// 全ての索引語が存在する文書があったので，索引語の結果→検索語の結果に変換する
			// 具体的には，第1索引語の結果を元にして結果セットを1つに取りまとめる
			// 全ての検索語は第1索引語の位置から開始され、かつ全ての索引語が存在する文書ファイルには
			// 必ず第1索引語が含まれていることから、全ての検索結果は，第1索引語の検索結果と同じである
			return seqOperationApplied[0];
		} catch(IndexNotFoundException e) {
			// 索引語のどれかがインデックスシステムに存在しなかった場合
			// 全ての索引語が連続した位置に存在しなければ失敗なので，エラー扱い
			return null;
		} catch(SearchException e) {
			throw e;
		} catch (ParserException e) {
			throw new SearchException(e);
		} catch (IndexAccessException e) {
			throw new SearchException(e);
		} catch (LogicalOperationException e) {
			throw new SearchException(e);
		}
	}
	/**
	 * 検索結果をソートするための比較ロジックを返す．
	 * @return 比較ロジック
	 */
	protected Comparator getComparator() {
		return sortingAlgorithm;
	}
}
