package com.ftinc.si.assist.test.web;

import java.awt.event.ActionEvent;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;

import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.NotFoundException;
import org.openqa.selenium.WebDriver;

import com.ftinc.si.assist.run.Messages;
import com.ftinc.si.assist.test.Fson;
import com.ftinc.si.assist.test.JSONRecord;
import com.ftinc.si.assist.test.Tool;
import com.ftinc.si.assist.test.gui.JSONChooser;

public class JQExec extends PageAction {
	private static HashMap<String, String> s_scripts = new HashMap<String, String>();

	public final ArrayList<String> doAction(WebDriver drv) {
		ArrayList<String> result = new ArrayList<String>();

		//正規表現でスキップ指定
		String raw_reg = (String)arg_map.get("value"); //$NON-NLS-1$
		if (":SKIP".equals(raw_reg)) { //$NON-NLS-1$
			result.add(""); //$NON-NLS-1$
			return result;
		}

		//-----以下、スクリプトを実行しようとする。----------
		//Locatorが指定されているなら、現れるまで待つ。
		waitUntil(drv);

		//ラムダ式の場合に解析してから実行する。
		final String t_src = scriptFromLambda(true);
		String t_script = s_objByXpath + t_src;
		try {
			JavascriptExecutor jsexe = (JavascriptExecutor)drv;
			Object t_obj = jsexe.executeScript(t_script);
			if (t_obj == null) {
				result.add("");//空文字なら"(?!.+)"とおる。 //$NON-NLS-1$
				return result;
			}
			result.add(t_obj.toString());
		} catch (Exception e) {
			result.add("JQ target=" + Messages.getString("JQExec.6") + e.getMessage()); //$NON-NLS-1$
		}
		return result;
	}

	private static final String s_objByXpath = "function _objByXPath(elm){return document.evaluate(elm,document,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);}"; //$NON-NLS-1$

	//ラムダ式の(x,y,..)部分の括弧内部が切り出されて、引数として指定されている。引数の配列を取り出す。
	private String[] argsList(String src)  {
		if (src != null && src.length() > 0) {
			try {
				ArrayList<String> esq_args = Fson.escapeBraket(src, "\"(");//"",()をエスケープ。,をむき出しにさせる。()は演算や関数の考慮。
				String[] ar_args = esq_args.get(0).split(","); //$NON-NLS-1$
				for (int i = 0; i < ar_args.length; i++) {
					ar_args[i] = Fson.restoreBraket(ar_args[i], esq_args);//元に戻す。
					ar_args[i] = ar_args[i].replaceFirst("=", Matcher.quoteReplacement("\t")); //=の代わりにtabを使う //$NON-NLS-1$ //$NON-NLS-2$
				}
				return ar_args;
			} catch (ParseException e) {
				Tool.logForTesting(e, src + " @JQExec.argList");
			}
		}
		return null;
	}


	private static final Pattern lambda_pat = Pattern.compile("\\s*\\(\\s*([^\n]+)\\s*\\)\\s*\\->\\s*\\{\\s*([\\s\\S]+)\\s*\\}\\s*\\z"); //$NON-NLS-1$

	//$x(xpath). .込みでマッチすることで誤認識を減らす。この括弧の中はシンボルである。
	private static final Pattern xelm_pat = Pattern.compile("\\$x\\(\"?(.+?)\"?\\)\\."); //$NON-NLS-1$

	//anotationは自動でｰ>の後に入れる。
	private static final Pattern srcid_pat = Pattern.compile("^@ScriptId\\s*\\(\\s*\"?([^\\n]+)\"?\\s*\\)"); //$NON-NLS-1$

	//scriptがラムダ式の場合、解析する。
	// (x=arg1, y=arg2)-> return x + y;       >>>      return arg1 + arg2;
	// convertMacroはマクロを展開するかどうかを指定。
	private String scriptFromLambda(boolean convertMacro) {
		Object src = arg_map.get("script"); //$NON-NLS-1$
		if (src == null) {
			return ""; //$NON-NLS-1$
		}

		String t_id = "";//sccriptのJsonTemplateID //$NON-NLS-1$
		Matcher t_m = srcid_pat.matcher(src.toString());
		if (t_m.find()) {
			t_id = t_m.group(1);
			src = s_scripts.get(t_id);
			if (src == null) {
				JSONRecord t_jrec = JSONRecord.getJSONRec("java.lang.String",t_id); //$NON-NLS-1$
				if (t_jrec == null) {
					throw new NotFoundException(Messages.getString("JQExec.11") + t_id + "."); //$NON-NLS-1$ //$NON-NLS-2$
				} else {
					src = t_jrec.content;
					s_scripts.put(t_id, (String) src);
				}
			}
			t_id = "@ScriptId(" + t_id + ")\n"; //$NON-NLS-1$ //$NON-NLS-2$
		}

		t_m = lambda_pat.matcher(src.toString());
		if (t_m.find()) {
			String[] t_args = argsList(t_m.group(1));

			//引数の値リストを作る。
			ArrayList<String> i_args = getArgValuesForLamda(t_args);

			if (convertMacro) {
				//実行用に変換する。
				String t_script = "{" + t_m.group(2) + "}"; //$NON-NLS-1$ //$NON-NLS-2$

				t_m = xelm_pat.matcher(t_script);
				while (t_m.find()) {
					String target = t_m.group(1);
					if ("xpath".equals(target)) {
						//$x(xpath)を変換する。
						String xpath = getXpath();
						t_script = t_script.replace("$x(xpath)", "_objByXPath(\"" + xpath + "\")");

					} else {
						//マッチングした括弧の中はシンボル。
						t_script = t_m.replaceAll(Matcher.quoteReplacement("_objByXPath(") + "$1" + Matcher.quoteReplacement(").")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
					}
				}
				for (int i = 0; i < t_args.length; i ++) {
					//埋め込み先のパターンstr_patで置き換える。
					String str_pat = "([\\.,\\{\\}\\(\\s\\-\\*/\\+\\[])" + t_args[i] + "([\\]\\s,\\);\\+\\-/\\*\\.\\(])"; //$NON-NLS-1$ //$NON-NLS-2$
					t_script = t_script.replaceAll(str_pat, "$1" + Matcher.quoteReplacement(i_args.get(i)) + "$2"); //$NON-NLS-1$ //$NON-NLS-2$
				}

				//{}を外す。
				t_script = t_script.substring(1, t_script.length() - 1);
				return t_script;
			} else {
				//WebElementGetterのTarget用に成型する。
				String head = "("; //$NON-NLS-1$
				for (int i = 0; i < t_args.length; i ++) {
					if (head.length() > 1) {
						head += ","; //$NON-NLS-1$
					}
					head += t_args[i] + "=" + Fson.restore(i_args.get(i)); //$NON-NLS-1$
				}
				head += ") -> "; //$NON-NLS-1$
				return t_id + head + t_m.group(2);
			}
		} else {
			if (!convertMacro) {
				src = t_id + src;
			}
			//ラムダ式でない場合
			return src.toString();
		}
	}

	//引数の値リストを作る。引数名がselector,xpathの場合、id,xpathから取り文字列として"で囲む。その他は、そのまま返す。
	private ArrayList<String> getArgValuesForLamda(String[] argNames) {
		//argNames:ラムダ式から抽出した変数名。t_list：arg_map.get("args")で指定された変数値。

		//返却するリスト
		ArrayList<String> t_result = new ArrayList<String>();

		//pathの場合、"を外したので、再度つける必要がある。
		for (int i = 0; i < argNames.length; i ++) {
			if (arg_map.containsKey(argNames[i])) {
				Object t_val = arg_map.get(argNames[i]);
				if (t_val != null) {
					if (t_val.toString().matches("^(false|true)$") || t_val.toString().matches("^\\-?[0-9]*\\.?[0-9]+$")) {
						t_result.add(t_val.toString());
					} else if (t_val.toString().matches("^\\$\\(.*\\)$")) {
						//オブジェクトの場合。引数の中はxpath
						Matcher t_m = xelm_pat.matcher(t_val.toString());
						if (t_m.find()) {
							//引数が$x(...)の場合、オブジェクトに変換する。
							t_result.add(t_m.replaceAll(Matcher.quoteReplacement("_objByXPath(") + "$1" + Matcher.quoteReplacement(")."))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
						}
					} else {
						//文字列なら"で囲む。
						String t_str = t_val.toString();
						t_str = t_str.replaceFirst("^[\\\"']?(.*)[\"']?$", "$1");
						t_result.add("\"" + t_str + "\"");
					}
				} else {
					t_result.add("null");
				}
			}
		}
		return t_result;
	}


	//Assert判定するActionかどうか
	protected boolean assertable() {
		Object src = arg_map.get("value"); //$NON-NLS-1$
		if (src != null) {
			return true; //$NON-NLS-1$
		}
		return false;
	}


	//WebElementGetterのTarget欄の値を拾い上げる。
	protected String getTarget() {
		return scriptFromLambda(false);
	}

	//以下、Assertと二重の書き込みになるが自動的に整合性は保たれる。
	//WebElementGetterのArgument欄の値を指定する。
	protected ArrayList<String> getLabelAndParams() {
		ArrayList<String> t_list2 = super.getLabelAndParams();
		t_list2.add((String)arg_map.get("value")); //$NON-NLS-1$
		return t_list2;
	}


	//PageActionEditorの第二列に書き込む内容を合成する。
	protected String getJsonElement(String target, String arg) {
		if (arg != null) {
			//確認の正規表現用
			arg_map.put("value", arg); //$NON-NLS-1$
		}

		if (target != null) {
			//targetが与えられている場合、そちらを採用。引数を分離する。
			// target: (x=.., y=..., ...) -> [script body]
			ArrayList<String> a_list = new ArrayList<String>();

			Matcher t_m = lambda_pat.matcher(target);
			if (t_m.find()) {
				//状態ができている。
				String[] t_args = argsList(t_m.group(1));// ["x\t...", "y\t...",...]
				String t_script = t_m.group(2);//[script body]

				String res_lambda = "("; //$NON-NLS-1$

				for (int i = 0; i < t_args.length; i ++) {
					//=は\tに置き換わっている。
					String[] temp = t_args[i].split("\t"); //$NON-NLS-1$
					if (temp.length > 0) {
						if (res_lambda.length() > 1) {
							res_lambda += ","; //$NON-NLS-1$
						}
						if (temp.length > 1) {
							if ("xpath".equals(temp[0])) { //$NON-NLS-1$
								//変数名がxpathの場合、xpathとして扱う。
								temp[1] = temp[1].replaceAll("^\"|\"$", ""); //$NON-NLS-1$ //$NON-NLS-2$
								arg_map.put("xpath", temp[1]); //$NON-NLS-1$
								a_list.add(""); //$NON-NLS-1$
							} else if ("selector".equals(temp[0]) && temp[1].startsWith("\"#")) { //$NON-NLS-1$ //$NON-NLS-2$
								//変数名がselectorで#から始まる。
								temp[1] = temp[1].replaceAll("^\"#|\"$", ""); //$NON-NLS-1$ //$NON-NLS-2$
								arg_map.put("id", temp[1]); //$NON-NLS-1$
								a_list.add(""); //$NON-NLS-1$
							} else {
								a_list.add(temp[1]);
							}
						}
						res_lambda += temp[0];
					}
				}
				res_lambda += ")"; //$NON-NLS-1$
				t_script = res_lambda + "-> " + t_script; //$NON-NLS-1$

				//アノテーションがある場合、それをスクリプトとする。。
				String t_an = (String)getAnnotationFromScript(target);
				if (t_an != null) {
					arg_map.put("script",t_an.toString()); //$NON-NLS-1$
				} else {
					arg_map.put("script",t_script); //$NON-NLS-1$
				}
			} else {
				//ラムダ式ではない。

				//アノテーションがある場合、不要な部分を落とす。
				t_m = srcid_pat.matcher(target);
				if (t_m.find()) {
					target = t_m.group();
				}
				arg_map.put("script", target); //$NON-NLS-1$
			}
		}
		return super.getJsonElement(null, null);
	}

	//アノテーションがあれば切り出して返す。なければnullを返す。
	private Object getAnnotationFromScript(Object src) {
		Matcher t_m = srcid_pat.matcher(src.toString());
		if (t_m.find()) {
			return t_m.group();
		}
		return null;
	}

	//必要に応じて、WebElementGetterに対してJsonテンプレートを返す。
	protected String getTemplateString(WebElementGetter wg) {
		ArrayList<JSONRecord> t_result = Tool._db().getJSONRecWhere("java.lang.String", "js_%", Tool.version); //$NON-NLS-1$ //$NON-NLS-2$
		String t_json = null;
		String t_id = null;
		if (t_result.size() == 1) {
			t_json = t_result.get(0).content;
			t_id = t_result.get(0).name;
		} else if (t_result.size() > 1) {
			JSONChooser t_ch = new JSONChooser(wg, t_result);
			t_ch.setVisible(true);
			if (t_ch.is_ok) {
				t_json = t_ch.m_json;
				t_id = t_ch.m_name;
			}
		}
		if (t_json != null) {
			s_scripts.put(t_id,  t_json);

			String t_jexe = (String)Tool.getObjectfromJSON(String.class, t_json);
			if (t_jexe != null) {
				//DBから取得したスクリプトはアノテーションを先頭に追加して返す。
				return "@ScriptId(" + t_id + ")\n" + t_jexe; //$NON-NLS-1$ //$NON-NLS-2$
			}
		}

		return null;
	}

	//WebElementGetterから呼ばれる。
	protected JPopupMenu getPopup(WebElementGetter wg, String cur_str) {
		ArrayList<JSONRecord> t_result = Tool._db().getJSONRecWhere("java.lang.String", "js_%", Tool.version); //$NON-NLS-1$ //$NON-NLS-2$

		JPopupMenu t_pop = new JPopupMenu();
		if (t_result != null && t_result.size() > 0) {
			//登録されているものがあれば、メニューに出す。
			t_pop.add(new JMenuItem(new JQAction(Messages.getString("JQExec.58"), wg, cur_str))); //$NON-NLS-1$
		}
		Object src = arg_map.get("script"); //$NON-NLS-1$
		if (src != null && src.toString().length() > 0) {
			//何かが編集されているならばメニューに出す。
			t_pop.add(new JMenuItem(new JQAction(Messages.getString("JQExec.59"), wg, cur_str))); //$NON-NLS-1$
		}

		JMenu t_menu = new JMenu(Messages.getString("JQExec.39")); //$NON-NLS-1$

		//xpathを含んでいるならば絞り込み文字を追加できる。メニューを出す。
		if (cur_str.matches("^[\\s\\S]*\\(.*xpath=.*\\)\\s*\\->[\\s\\S]*$")) { //$NON-NLS-1$
			String t_path = cur_str.replaceFirst("^\\(.*xpath=\"([^\"]+)\".*\\)\\s*\\->[\\s\\S]*$", "$1"); //$NON-NLS-1$ //$NON-NLS-2$
			if (t_path.contains("input")) { //$NON-NLS-1$
				//inputタグを含んでいる
				t_menu.add(new JMenuItem(new InsertAction(Messages.getString("JQExec.44"), "EqualsText@", wg, cur_str))); //$NON-NLS-1$ //$NON-NLS-2$
				t_menu.add(new JMenuItem(new InsertAction(Messages.getString("JQExec.46"), "ContainsText@", wg, cur_str))); //$NON-NLS-1$ //$NON-NLS-2$
			} else {
				t_menu.add(new JMenuItem(new InsertAction(Messages.getString("JQExec.48"), "EqualsText", wg, cur_str))); //$NON-NLS-1$ //$NON-NLS-2$
				t_menu.add(new JMenuItem(new InsertAction(Messages.getString("JQExec.50"), "ContainsText", wg, cur_str))); //$NON-NLS-1$ //$NON-NLS-2$
				t_menu.add(new JMenuItem(new InsertAction(Messages.getString("JQExec.2"), "HasTag", wg, cur_str))); //$NON-NLS-1$ //$NON-NLS-2$
			}
		}
		t_pop.add(t_menu);
		return t_pop;
	}

	//popupのコマンド文字列を受けて、このメソッドに中継する。
	//中継だけなので、アクションはこのクラスのみ。
	class JQAction extends AbstractAction {
		 String m_name;
		 String m_src;
		 WebElementGetter m_master;

		 JQAction(String name, WebElementGetter master, String src) {
			putValue(Action.NAME, name);
			m_name = name;
			m_master = master;
			m_src = src;
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			if (m_name.equals(Messages.getString("JQExec.0"))) { //$NON-NLS-1$
				m_master.setTargetFromPopup(getTemplateString(m_master));
			} else if (m_name.equals(Messages.getString("JQExec.1"))) { //$NON-NLS-1$
				String t_str = getJsonElement(m_src, ""); //$NON-NLS-1$
				t_str = t_str.replaceFirst("^[\\s\\S]*\"script\":\"([^\"]*)\"[\\s\\S]*$", "$1"); //$NON-NLS-1$ //$NON-NLS-2$
				t_str = Fson.restore(t_str);
				m_master.registerTargetFromPopup("java.lang.String", t_str); //$NON-NLS-1$
			}
		}
	}

}
