/*
 * Copyright (c) 2005- Shinji Kashihara.
 * All rights reserved. This program are made available under
 * the terms of the Eclipse Public License v1.0 which accompanies
 * this distribution, and is available at epl-v10.html.
 */
package jp.sourceforge.mergedoc.pleiades.resource;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import jp.sourceforge.mergedoc.pleiades.log.Logger;

import org.apache.commons.io.IOUtils;

/**
 * ヘルプ HTML パーサーです。
 * <p>
 * @author cypher256
 */
@SuppressWarnings("serial")
public class HelpHtmlParser implements Iterable<HelpHtmlParser.HtmlFragment>{

	/** ロガー */
	private static final Logger log = Logger.getLogger(HelpHtmlParser.class);

	/** HTML -> Java 文字セットマッピング */
	private static final Map<String, String> htmlJavaCharsetMap = new HashMap<String, String>(){
		{
			put("shift_jis",	"Windows-31J");
		}
		@Override
		public String get(Object key) {
			String htmlCharset = (String) key;
			String javaCharset = super.get(htmlCharset.toLowerCase());
			return javaCharset == null ? htmlCharset : javaCharset;
		}
	};

	/** 入力バイト列 */
	private byte[] inputBytes;

	/** 入力文字列 */
	private String inputString;

	/** HTML ブロック・タグ・マッチャー */
	private Matcher blockTagMatcher;

	/** HTML ブロック・タグ・マッチャーの現在位置 */
	private int blockTagMatcherStart;

	/** タグ・マッチャー */
	private Matcher tagMatcher;

	/** 文字列出力バッファー */
	private StringBuffer outputBuffer;

	/**
	 * ヘルプ HTML パーサーを構築します。
	 * 引数の入力ストリームはクローズされません。
	 * <p>
	 * @param is HTML 入力ストリーム
	 */
	public HelpHtmlParser(InputStream is) {

		final String EN_CHARSET = "ISO-8859-1";

		try {
			inputBytes = IOUtils.toByteArray(is);
			inputString = new String(inputBytes, EN_CHARSET).trim();

			if (inputString.startsWith("<!DOCTYPE") ||
				inputString.startsWith("<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE")) {

				// meta タグの charset に従い文字列に変換
				String charsetPattern = "(?i)(?s).+?text/html; charset=([^\"]+).+";
				if (inputString.matches(charsetPattern)) {

					String charset = inputString.replaceFirst(charsetPattern, "$1");
					if (!charset.equalsIgnoreCase(EN_CHARSET)) {

						String javaCharset = htmlJavaCharsetMap.get(charset);
						inputString = new String(inputBytes, javaCharset);
					}
				} else {
					log.warn("HTML の charset が見つからないため、%s でロードしました。\n%s",
						EN_CHARSET, (inputString.length() > 500) ? inputString.substring(0, 500) : inputString);
				}

				String html4StrictBlockElements =
					"p|div|table|dl|ul|ol|form|address|blockquote|h[1-6]|fieldset|hr|pre";
				String blockElementsEx = "li|title|th|td|" + html4StrictBlockElements;
				String blockTags = "(?i)(?s)<(|/)(" + blockElementsEx + ")(>|\\s+[^>]*>)";
				Pattern blockTagPattern = Pattern.compile(blockTags + "(.+?)" + blockTags);
				blockTagMatcher = blockTagPattern.matcher(inputString);
				outputBuffer = new StringBuffer();
			}

		} catch (Exception e) {

			log.error(e, "ヘルプ HTML のパースに失敗しました。");
			throw new IllegalArgumentException(e);
		}
	}

	/**
	 * フラグメントのイテレーターを取得します。
	 */
	public Iterator<HtmlFragment> iterator() {

		return new Iterator<HtmlFragment>() {

			private int id;
			private HtmlFragment f;
			private HtmlFragment fNext;

			private boolean isNoText(String group123, String group2) {

				if (group123.matches("(?i)(?s)<pre>.+</pre>") ||
					group2.trim().length() == 0 ||
					group2.matches("(?i)(?s)(" +
						".+<body.+|" +			// body 開始タグを含む
						"<font[^>]*>|" +		// font タグのみ
						"<[\\w/]+>|" +			// 属性なし単一タグ
						"</tr>.*<tr.*>" +		// tr 終了開始
						")\\s*")) {

					return true;
				}
				// 1 つ以上のタグのみで構成 (テキストがない) かつ
				// alt、title 属性なし
				if (group2.matches("(?i)(?s)(<[^>]+>\\s*)+") &&
					!group2.matches("(?i)(?s).*?\\s(alt|title)=.*")) {

					return true;
				}
				return false;
			}

			public boolean hasNext() {

				if (fNext != null) {
					f = fNext;
					fNext = null;
					return true;
				}
				if (blockTagMatcher == null || !blockTagMatcher.find(blockTagMatcherStart)) {
					return false;
				}
				outputBuffer.append(inputString.substring(blockTagMatcherStart, blockTagMatcher.start()));
				String group123 = blockTagMatcher.group();
				Pattern pat = Pattern.compile("(?i)(?s)^(<.+?>\\s*)(.+)(\\s*<.+>)$");
				tagMatcher = pat.matcher(group123);
				if (!tagMatcher.find()) {
					throw new IllegalArgumentException("HTML フラグメント不正：" + group123);
				}
				blockTagMatcherStart = blockTagMatcher.end() - (tagMatcher.end() - tagMatcher.end(2));
				String group2 = tagMatcher.group(2);
				String group12 = tagMatcher.group(1) + tagMatcher.group(2);

				// 読み飛ばし
				if (isNoText(group123, group2)) {
					outputBuffer.append(group12);
					return hasNext(); // 再帰
				}

				// フラグメント作成
				f = new HtmlFragment(id++);
				f.starts = tagMatcher.group(1);
				f.text = tagMatcher.group(2);

				// 末尾の font 開始タグ
				String p = "(?i)(?s)^(.+)(\\s*<font.*>)$";
				Matcher m = Pattern.compile(p).matcher(f.text);
				if (m.find()) {
					f.text = m.group(1);
					f.ends = m.group(2) + f.ends;
				}
				// 先頭の font、p 終了タグ
				p = "(?i)(?s)^(<(p|/font)>\\s*)(.+)$";
				m = Pattern.compile(p).matcher(f.text);
				if (m.find()) {
					f.starts += m.group(1);
					f.text = m.group(3);
				}
				// 先頭の翻訳不要な a タグ (テキストが定数)
				p = "(?i)(?s)^((|「)<a\\s+.+>[A-Z1-9_]+?</a>[\\s-」]*)(.+)$";
				m = Pattern.compile(p).matcher(f.text);
				if (m.find()) {
					f.starts += m.group(1);
					f.text = m.group(3);
				}
				// 末尾の翻訳不要な a タグ (テキストが定数)
				p = "(?i)(?s)^(.+?)(\\s*(|\\()\\s*<a\\s+.+>[A-Z1-9_]+?</a>\\s*(|\\)))$";
				m = Pattern.compile(p).matcher(f.text);
				if (m.find()) {
					f.text = m.group(1);
					f.ends = m.group(2) + f.ends;
				}
				// 先頭開始タグ、末尾終了タグのセット (無くなるまで)
				while (true) {
					p = "(?i)(?s)^(\\s*<[^>]+>\\s*)(.+?)(\\s*</[^>]+>\\s*)$";
					m = Pattern.compile(p).matcher(f.text);
					if (m.find()) {
						String m1 = m.group(1);
						if (m1.contains("alt=") || m1.contains("title=")) {
							break;
						}
						f.starts += m.group(1);
						f.text = m.group(2);
						f.ends = m.group(3) + f.ends;
						continue;
					}
					break;
				}
				// 末尾の br タグ
				p = "(?i)(?s)^(.+?)((\\s*<br>\\s*)+)$";
				m = Pattern.compile(p).matcher(f.text);
				if (m.find()) {
					f.text = m.group(1);
					f.ends = m.group(2) + f.ends;
				}
				// 単一タグ、alt、title 属性あり
				p = "(?i)(?s)^(<[^>]+?\\s(alt|title)=\")(.+?)(\"[^>]*>\\s*)$";
				m = Pattern.compile(p).matcher(f.text);
				if (m.find()) {
					f.starts += m.group(1);
					f.text = m.group(3);
					f.ends = m.group(4) + f.ends;

					p = "(?i)(?s)^(\"\\s+(alt|title)=\")(.+?)(\".*)$";
					m = Pattern.compile(p).matcher(f.ends);
					if (m.find()) {
						f.ends = "";
						fNext = new HtmlFragment(id++);
						fNext.starts = m.group(1);
						fNext.text = m.group(3);
						fNext.ends = m.group(4);
					}
				}
				return true;
			}

			public HtmlFragment next() {
				return f;
			}

			public void remove() {
				throw new UnsupportedOperationException();
			}
		};
	}

	/**
	 * HTML フラグメント・クラスです。
	 */
	public class HtmlFragment {

		/** HTML 内でフラグメントをユニークに識別する ID */
		public final int id;

		/** 先頭ブロック要素 */
		private String starts = "";

		/** テキスト (ブロック要素以外の部分) */
		private String text = "";

		/** テキスト・バックアップ */
		private String textBackup = "";

		/** 末尾ブロック要素 */
		private String ends = "";

		/**
		 * HTML フラグメントを構築します。
		 * @param id HTML 内でフラグメントをユニークに識別する ID
		 */
		HtmlFragment(int id) {
			this.id = id;
		}

		/**
		 * フラグメント内のテキスト部分を取得します。
		 * この文字列には HTML ブロック要素 (h2 や p など) は含まれませんが、
		 * インライン要素 (a i b code font) が含まれる場合があります。
		 * <p>
		 * テキストに含まれる改行は半角空白に置換されます。
		 * ただし、改行前後のいずれかの文字が非 ASCII、つまり日本語の場合、改行は除去されます。
		 * <p>
		 * @return テキスト
		 */
		public String getText() {
			textBackup = text.replaceAll("(\\r\\n|\\r)", "\n");
			textBackup = textBackup.replaceAll("(\\p{ASCII})\\n", "$1 ");
			textBackup = textBackup.replaceAll("\\n(\\p{ASCII})", " $1");
			textBackup = textBackup.replace("\n",    "");
			textBackup = textBackup.replaceAll("\\s+", " ");
			return textBackup;
		}

		/**
		 * フラグメント内のテキスト部分をセットします。
		 * @param text テキスト
		 */
		public void setText(String text) {

			// getText したものと変更がない場合は、改行は取らないほうが良いため復元
			if (text.equals(textBackup)) {
				text = this.text;
			}
			String s = starts + text + ends;
			outputBuffer.append(s);
		}

		@Override
		public String toString() {
			String p = "id" + id + ":group";
			return     p + "1| " + starts +
				"\n" + p + "2| " + getText() +
				"\n" + p + "3| " + ends;
		}

		/**
		 * 先頭ブロック要素を取得します。
		 * @return 先頭ブロック要素
		 */
		public String getStarts() {
			return starts;
		}

		/**
		 * 末尾ブロック要素を取得します。
		 * @return 末尾ブロック要素
		 */
		public String getEnds() {
			return ends;
		}
	}

	/**
	 * イテレート処理結果を日本語ヘルプ HTML 入力ストリームとして取得します。
	 * <p>
	 * @return 日本語ヘルプ HTML 入力ストリーム
	 */
	public InputStream getInputStream() {

		if (blockTagMatcher == null) {
			return new ByteArrayInputStream(inputBytes);
		}
		outputBuffer.append(inputString.substring(blockTagMatcherStart));
		String s = outputBuffer.toString().replaceFirst("(text/html; charset=)[^\"]+", "$1UTF-8");
		return new ByteArrayInputStream(Strings.getBytes(s, "UTF-8"));
	}
}
