/*
 * $Id: DefaultSummaryGenerator.java,v 1.3 2006/01/31 17:10:01 rampil Exp $
 * Copyright (c) 2004 LOGICAL-PARADOX.ORG
 */
package org.logical_paradox.koike.core.summary;

import java.util.ArrayList;
import java.util.Collections;
import org.logical_paradox.common.util.StringUtils;

/**
 * 標準的なサマリージェネレータ<br>
 * <br>
 * 以下の順で、範囲を特定する<br>
 * <li>検索語の種類</li>
 * <li>検索語の距離</li>
 * <li>検索語の数</li>
 * @author satoshi akabane@logical-paradox.org
 * @version $Revision: 1.3 $
 */
public class DefaultSummaryGenerator implements SummaryGenerator {
	/** デフォルトのサマリー長 */
	public static final int DEFAULT_SUMMARY_LENGTH = 100;
	/** サマリー長 */
	private final int summaryLength;

	/**
	 * コンストラクタ
	 */
	public DefaultSummaryGenerator() {
		summaryLength = DEFAULT_SUMMARY_LENGTH;
	}
	/**
	 * コンストラクタ
	 * 主にUTで使用するためのもの
	 * @param summaryLength_ 要約の長さ
	 */
	public DefaultSummaryGenerator(int summaryLength_) {
		summaryLength = summaryLength_;
	}
	/**
	 * サマリーを生成する
	 * @param document 元の文書
	 * @param terms 検索条件として指定されたキーワード
	 * @return 生成されたサマリー
	 */
	public String createSummary(String document, String[] terms) {
		// 検索語ごとの出現位置情報
		Integer[] locations = null;
		// 全ての検索語の出現位置情報
		ArrayList<Location> summaryStartPoints = new ArrayList<Location>();

		// 全ての検索語の出現位置を検索する
		for(int i = 0; i < terms.length; i++) {
			locations = StringUtils.searchWordIndexes(document, terms[i]);
			for(int j = 0; j < locations.length; j++) {
				summaryStartPoints.add(new Location(i, terms[i], locations[j].intValue()));
			}
		}
		// 指定された検索語が指定された文字列中に存在しない場合はブランクを返す
		if(summaryStartPoints.size() == 0) {
			return "";
		}
		// 位置情報のソート(昇順)
		Collections.sort(summaryStartPoints);

		// 要約として最適な位置を計測する
		Location[] summaryStartPoint = (Location[])summaryStartPoints.toArray(new Location[0]);
		ArrayList<EvaluationPoint> evals = new ArrayList<EvaluationPoint>();
		String candidate = null;
		for(int i = 0; i < summaryStartPoint.length; i++) {
			candidate = document.substring(summaryStartPoint[i].pos);
			if(candidate.length() > summaryLength) {
				// 最大サマリー長より長く文字列が取れた場合、途中でカットする
				candidate = candidate.substring(0, summaryLength);
			} else if(candidate.length() < summaryLength) {
				// 指定されたサマリー長より短い場合、必要数だけ補完する
				int pos = document.length() - summaryLength;
				if(pos < 0) {
					pos = 0;
				}
				candidate = document.substring(pos);
			}
			// 評価ポイントを計測する
			evals.add(eval(candidate, summaryStartPoint[i].pos, terms));
		}

		// 評価ポイントを評価の高い順にソートして，一番最初にきたものが要約
		Collections.sort(evals);
		EvaluationPoint best = (EvaluationPoint)evals.get(0);

		StringBuffer sb = new StringBuffer();
		String summary = best.getSummary();
		if(document.startsWith(summary) == false) {
			sb.append("..");
		}
		sb.append(summary);
		if(document.endsWith(summary) == false) {
			sb.append("..");
		}
		
		return sb.toString();
	}
	/**
	 * 指定されたサマリーの評価ポイントを計測する
	 * @param summary サマリー
	 * @param pos 位置情報
	 * @param locations 検索語の位置情報(出現位置順でソートされている)
	 * @return 評価ポイント
	 */
	protected EvaluationPoint eval(String summary, int pos, String[] terms) {
		int occupied = 0;
		int variation = 0;
		String remain = summary;

		// 要約の範囲内に含まれる語数を計測する
		for(int i = 0; i < terms.length; i++) {
			occupied += StringUtils.searchWordIndexes(summary, terms[i]).length * terms[i].length();
			String replaced = remain.replaceAll(terms[i], "");
			if(replaced.length() < remain.length()) {
				variation++;
			}
		}
		int distance = occupied * 100 / summary.length();

		return new EvaluationPoint(summary, pos, distance, variation * 100 / terms.length);
	}
	/**
	 * 検索語の出現位置を表現する位置情報クラス
	 * @author satoshi akabane@logical-paradox.org
	 * @version $Revision: 1.3 $
	 */
	class Location implements Comparable {
		/** 検索語種類番号 */
		final int index;
		/** 検索語 */
		final String term;
		/** 出現位置情報 */
		final int pos;

		/**
		 * コンストラクタ
		 * @param term_ 検索語
		 * @param pos_ 位置情報
		 */
		public Location(int index_, String term_, int pos_) {
			index = index_;
			term = term_;
			pos = pos_;
		}
		/**
		 * 比較する
		 * 位置情報を小さい順にソートするため
		 * @param o 比較対象
		 * @return 0:同じ 1:大きい -1:小さい
		 */
		public int compareTo(Object o) {
			Location l = (Location)o;
			if(l.pos == pos) {
				return 0;
			} else if(pos < l.pos) {
				return -1;
			} else {
				return 1;
			}
		}
	}
	/**
	 * 要約の開始位置スコア
	 * @author satoshi akabane@logical-paradox.org
	 * @version $Revision: 1.3 $
	 */
	class EvaluationPoint implements Comparable {
		/** サマリー */
		private String summary;
		/** 位置 */
		private int pos;
		/** 距離と回数のスコア */
		private int distance;
		/** 検索語の出現バリエーションのスコア */
		private int variation;

		/** 要約開始位置としてのスコア */
		private int locationScore;

		/**
		 * コンストラクタ
		 * @param pos_ 要約開始位置
		 * @param distance_ 距離と数によるスコア
		 * @param variation_ 多様性によるスコア
		 */
		public EvaluationPoint(String summary_, int pos_, int distance_, int variation_) {
			summary = summary_;
			pos = pos_;
			distance = distance_;
			variation = variation_;
			
			computeTotalScore();
		}
		/**
		 * サマリーを返す
		 * @return サマリー
		 */
		public String getSummary() {
			return summary;
		}
		/**
		 * 要約開始位置としてのスコアを計算する
		 */
		protected void computeTotalScore() {
			locationScore = (int)distance + variation * 9;
		}
		/**
		 * 比較する
		 * @param o 比較対象
		 * @return 0:同じ 1:大きい -1:小さい
		 */
		public int compareTo(Object o) {
			EvaluationPoint e = (EvaluationPoint)o;
			if(e.getLocationScore() == getLocationScore()) {
				return 0;
			} else if(e.getLocationScore() > getLocationScore()) {
				return 1;
			} else {
				return -1;
			}
		}
		/**
		 * 要約開始位置としてのスコアを返す
		 * @return 要約開始位置としてのスコア
		 */
		public int getLocationScore() {
			return locationScore;
		}
		/**
		 * 要約開始位置を返す
		 * @return 要約開始位置
		 */
		public int getPos() {
			return pos;
		}
		/**
		 * 検索語間の距離と数によるスコアを返す
		 * @return　検索語間の距離と数のスコア
		 */
		public int getDistanceScore() {
			return distance;
		}
		/**
		 * 検索語の出現種類数によるスコアを返す
		 * @return 検索語の出現種類数によるスコア
		 */
		public int getVariationScore() {
			return variation;
		}
	}
}
