package ash.gui.editor.core;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.io.File;
import javax.swing.JTextArea;
import java.util.List;
import java.util.LinkedList;
import ash.util.FileUtil;
import ash.gui.core.GUIAdapter;

/**
 * ファイルパス名、FQCN、grepの検索結果、コンパイルエラー、実行時エラーなどから、
 * 該当するファイルをオープンする機能を提供する。
 * 該当するファイルをサーチする際のルートディレクトリをサーチパスのリストに
 * 登録しておくことができる。
 * <p>
 * テキストエディタまたはコンソールのテキスト表示領域で、ファイルパス名の
 * 文字列を選択しopenViewer(filePath)メソッドを呼ぶか、
 * ファイルパス名と行番号の情報を持つ行にカーソルを位置付けて
 * openFromTextArea(...)メソッドを呼ぶことによって、
 * 該当ファイルをオープンできる。
 */
public class AshGUIViewer {
	/**
	 * サーチパスのリスト：
	 * サーチパスとして登録されたディレクトリは、ファイルオープンの際に
	 * その配下のファイルが候補になる。
	 */
	private static List<String> searchPath = new LinkedList<String>();

	/**
	 * setspath <path>* の実装
	 * <p> 指定されたディレクトリ群をサーチパスに設定する。
	 */
	public static int resetSearchPath(String[] args) {
		searchPath.clear();
		return addSearchPath(args);
	}
	/**
	 * addspath <path>* の実装
	 * <p> 指定されたディレクトリ群をサーチパスに追加する。
	 */
	public static int addSearchPath(String[] args) {
		if(args.length == 0) {
			for(String path : searchPath) System.out.println(path);
		} else {
			int i = 0;
			for(String root : args) {
				File f = GUIAdapter.findFile(root);
				if(f != null && f.isDirectory()) {
					String path = FileUtil.getPath(f);
					if(!searchPath.contains(path)) searchPath.add(i++, path);
				} else {
					System.err.println("directory requred: " + root);
				}
			}
		}
		return 0;
	}

	/**
	 * 指定されたファイルを開く。
	 * @param filePath オープンするファイルのパス名またはFQCN
	 * @return 指定されたファイルがオープンできたらtrue
	 */
	public static boolean openViewer(String filePath) {
		if(filePath == null) return false;
		if(openWithSearchPath(filePath)) return true;
		// FQCNからJavaソースファイルを探す。
		String path = filePath.replace('.', '/');
		if(openWithSearchPath(path + ".java")) return true;
		// FQCNから内部クラス名を取り除いたJavaソースファイルを探す。
		int i = path.lastIndexOf('/');
		if(i > 0) return openWithSearchPath(path.substring(0,i) + ".java");
		return false;
	}
	/**
	 * 指定されたファイルを開き、指定された行番号にカーソルを位置付ける。
	 * ファイルが現在ディレクトリで見つからなかったらサーチパスを探す。
	 * @param filePath オープンするファイルのパス名またはFQCN
	 * @return 指定されたファイルがオープンできたらtrue
	 */
	private static boolean openWithSearchPath(String filePath) {
		if(_openViewer(filePath, "1")) return true;
		for(String root : searchPath) {
			if(_openViewer(root + filePath, "1")) return true;
		}
		return false;
	}

	/**
	 * 指定されたテキストエリア中の現在のカーソル位置から、ファイルパス名と
	 * 行番号を探し、見つかったらそのファイルのその行番号でオープンする。
	 * @param ta テキストエリア
	 * @return 見つかりオープンできたらtrue
	 */
	public static boolean openFromTextArea(JTextArea ta) {
		String path = ta.getSelectedText();
		if(path != null && openViewer(path.trim())) return true;
		if(openFromTextArea(ta, "")) return true;
		for(String root : searchPath) {
			if(openFromTextArea(ta, root)) return true;
		}
		return false;
	}

	/**
	 * 指定されたテキストエリア中の現在のカーソル位置から、ファイルパス名と
	 * 行番号を探し、見つかったらそのファイルをオープンする。
	 * @param ta テキストエリア
	 * @param root ルートのパス名
	 * @return 見つかりオープンできたらtrue
	 */
	public static boolean openFromTextArea(JTextArea ta, String root) {
		if(!guiViewer.searchTarget(ta, root)) return false;
		return _openViewer(guiViewer.filePath, guiViewer.lineNum);
	}

	/**
	 * 指定されたファイルをオープンし、指定された行番号にカーソルを位置付ける。
	 * @param filePath オープンするファイルのパス名
	 * @param lineNum カーソルを位置付ける行番号
	 * @return 見つかりオープンできたらtrue
	 */
	private static boolean _openViewer(String filePath, String lineNum){
		File f = GUIAdapter.findFile(filePath);
		if(f == null || !f.isFile()) return false;
		if(filePath.startsWith("~/"))
			filePath = System.getenv("HOME") + filePath.substring(1);
		GUIAdapter.openViewer(filePath, lineNum);
		return true;
	}

	/**
	 * インスタンスメンバー
	 */
	private AshGUIViewer() { }

	private static AshGUIViewer guiViewer = new AshGUIViewer();

	private String filePath;	// OpenViewer()メソッドで使用するファイルのパス名
	private String lineNum;		// OpenViewer()メソッドで使用する行番号の文字列

	/**
	 * 指定されたテキストエリア中の現在のカーソル位置から、ファイルパス名と
	 * 行番号を探し、フィールド変数のfilePathとlineNumにそれぞれ設定する。
	 * @param ta テキストエリア
	 * @param root ルートのパス名
	 * @return ファイルが見つかったらtrue
	 */
	private boolean searchTarget(JTextArea ta, String root) {
		return
			guiViewer.searchRuntimeErrorFormat(ta, root) ||
			guiViewer.searchGrepFormat(ta, root);
	}

	// grep の出力パターン
	static final String GREP_PAT =
		"(([a-zA-Z]:)?([^:*?'<>\"\\(\\)]+)):\\s*(\\d+):";
	//   $1=filePath  $3=path                   $4=lineNum
	// C:/Java/JUnit/junit4.4/src/junit/textui/TestRunner.java: 30:
	// src/ash/command/AshGrep.java: 13:
	private static Pattern grepPat = Pattern.compile(GREP_PAT);

	/**
	 * 指定されたテキストエリア中の現在のカーソル位置から、grep の出力パターンの
	 * ファイルパス名と行番号を探し、フィールド変数のfilePathとlineNumに
	 * それぞれ設定する。
	 * @param ta テキストエリア
	 * @param root ルートのパス名
	 * @return ファイルが見つかったらtrue
	 */
	private boolean searchGrepFormat(JTextArea ta, String root) {
		Matcher m = grepPat.matcher(getCurrentLine(ta));
		if(!m.find()) return false;
		filePath = m.group(1);
		lineNum = m.group(4);
		// 絶対パスでない場合は、root をつける。
		if(!m.group(3).startsWith("/")) filePath = root + filePath;
		return true;
	}

	// 実行時エラーの出力パターン
	static final String FQCN_PAT = "([\\w.]*?)\\.?\\w+(\\$\\w+)?\\.[\\w\\$<>]+";
	//                              $1=package        $2=innerClass
	static final String ERR_PAT = FQCN_PAT + "\\((\\w+\\.java):\\s*(\\d+)\\)";
	//                                           $3=fileName       $4=lineNum
	// 	at junit.samples.SimpleTest.testDivideByZero(SimpleTest.java:52)
	//	at ash.gui.AshGrepDialog$AshGrepPanel.execGrep(AshGrepDialog.java:193)
	private static Pattern runtimeErrPat = Pattern.compile(ERR_PAT);

	/**
	 * 指定されたテキストエリア中の現在のカーソル位置から、
	 * java 実行時エラーの出力パターンのファイルパス名と行番号を探し、
	 * フィールド変数のfilePathとlineNumにそれぞれ設定する。
	 * @param ta テキストエリア
	 * @param root ルートのパス名
	 * @return ファイルが見つかったらtrue
	 */
	private boolean searchRuntimeErrorFormat(JTextArea ta, String root) {
		Matcher m = runtimeErrPat.matcher(getCurrentLine(ta));
		if(!m.find()) return false;
		String path = m.group(1);		// junit.samples, ash.gui
		String fileName = m.group(3);	// SimpleTest.java, AshGrepDialog.java
		path = (path != null) ? path.replace('.', '/') + "/" : "";
		filePath = root + path + fileName;
		// filePath = junit/samples/SimpleTest.java
		lineNum = m.group(4);			// 52
		return true;
	}

	public static String getCurrentLine(JTextArea ta) {
		if(!guiViewer.currentLine(ta)) return "";
		return ta.getText().substring(guiViewer.start, guiViewer.end);
	}
	public static void selectCurrentLine(JTextArea ta) {
		if(guiViewer.currentLine(ta)) {
			ta.setSelectionStart(guiViewer.start);
			ta.setSelectionEnd(guiViewer.end);
		}
	}

	private int start = 0;
	private int end = 0;

	private boolean currentLine(JTextArea ta) {
		String text = ta.getText();
		int index = ta.getCaretPosition();
		if(text.length() <= index || text.charAt(index) == '\n') index--;
		if(0 <= index) {
			start = text.lastIndexOf('\n', index) + 1;
			end = text.indexOf('\n', index);
			if(end < 0) end = text.length();
			return (start < end);
		}
		return false;
	}
}
