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

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.Point;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.DefaultListModel;
import javax.swing.JDialog;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.openqa.selenium.WebDriver;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.ftinc.si.assist.run.VCentral;
import com.ftinc.si.assist.test.Fson;
import com.ftinc.si.assist.test.Tool;

public class PageAssistMenu extends JDialog {
	private JList<String> _list;
	private ArrayList<ArrayList<String>> m_history;//候補の履歴。キー絞り込みの際に、以前の状態を保持する。
	private String m_head = "";//キー入力による絞り込みキーワード
	
	private DefaultListModel<String> m_model;
	
	//置き換える文字列
	public String m_answer = null;
	
	//引数の保存
	private WebDriver m_driver;
	private String m_src;
	private char m_c;
	private JDialog me;//EventQueue.invokeLaterのための便宜

	public int countCandidate() {
		return m_model.getSize();
	}
	
	//直接要素を入れる場合のメソッド。
	public void addCandidates(ArrayList<String> list) {
		Collections.sort(list);
		for (int i = 0; i < list.size(); i++) {
			m_model.addElement(list.get(i));
		}
	}
	
	//modeはxpathもしくはcss。
	public PageAssistMenu(JDialog owner, WebDriver drv, char c, String src, Point pt) {
		super(owner, true);
		me = this;
		
		//履歴の入れ物
		m_history = new ArrayList<ArrayList<String>>();
		m_driver = drv;
		m_src = src;
		m_c = c;
		m_xmldoc = null;
		
		setBounds(pt.x, pt.y, 300, 200);
		setUndecorated(true);
		
		JScrollPane scrollPane = new JScrollPane();
		getContentPane().add(scrollPane, BorderLayout.CENTER);
		
		m_model = new DefaultListModel<String>();
		_list = new JList<String>(m_model);
		_list.addComponentListener(new ComponentAdapter() {
			@Override
			public void componentShown(ComponentEvent e) {
				//もし、表示するに値しないなら、閉じる。
				if (m_model.getSize() == 0) {
					setVisible(false);
				}
			}
		});
		_list.addFocusListener(new FocusAdapter() {
			@Override
			public void focusLost(FocusEvent e) {
				setVisible(false);
			}
		});
		_list.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {
				int i = _list.getSelectedIndex();
				if (m_model.getElementAt(i) != null) {
					setVisible(false);
					m_answer = m_model.getElementAt(i);
				}
			}
			@Override
			public void mouseExited(MouseEvent e) {
				setVisible(false);
			}
		});
		_list.addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent e) {
				if (e.getKeyCode() == KeyEvent.VK_ENTER) {
					//結果を回答に転記。
					int i = _list.getSelectedIndex();
					if (m_model.getElementAt(i) != null) {
						m_answer = m_model.getElementAt(i);
					}
					setVisible(false);
				} else if (e.getKeyCode() == KeyEvent.VK_ESCAPE || e.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
					//エスケープなので終わる。
					setVisible(false);
				} else if (e.getKeyCode() == KeyEvent.VK_LEFT) {
					//左矢印で前に戻る。
					if (m_history.size() == 0) {
						//何もないので終わる。
						setVisible(false);
					} else {
						//一つ前の状態にする。
						ArrayList<String> t_list = m_history.get(m_history.size() - 1);
						m_head = m_head.substring(0, m_head.length() - 1);
						
						m_model.removeAllElements();
						for (int i = 0; i < t_list.size(); i++) {
							m_model.addElement(t_list.get(i));
						}
						m_history.remove(t_list);
					}
				} else {
					String t_char = Character.toString(e.getKeyChar());
					if (t_char.matches("[\\w_\\-#!@/]")) {
						//文字の場合、絞り込む。
						ArrayList<String> t_list = new ArrayList<String>();
						
						m_head += t_char;
						for (int i = m_model.getSize() - 1; i >= 0; i--) {
							String t_elm = (String)m_model.get(i);
							t_list.add(t_elm);
							if (!t_elm.toLowerCase().startsWith(m_head)) {
								m_model.remove(i);
							}
						}
						Collections.sort(t_list);
						m_history.add(t_list);
						
						//空になるということは、そのまま入力であるので、絞り込みキーワードが答えである。メニューを閉じる。
						if (m_model.getSize() == 0) {
							m_answer = m_head;
							setVisible(false);
						}
					}
				}
			}
		});
		scrollPane.setViewportView(_list);
		if (src != null) {
			initMenu();
		}
		
		//_listがフォーカスを持つようにする。
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				//このスレッドが終わるまではフォーカスされないのでタイマーでやる。（表示後でないとフォーカスされないため）
				try {
					Thread.sleep(500);
					SwingUtilities.getWindowAncestor(me).toFront();
					if (countCandidate() > 0) {
						_list.setSelectedIndex(0);
						_list.requestFocusInWindow();
					} else {
						setVisible(false);
					}
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		});
	}

	
	//xpath入力のため属性値のリストを作る。
	private ArrayList<String> getAttrListForXPATH() {
		Pattern t_pat0 = null;
		Pattern t_pat1 = null;
		Pattern t_pat2 = null;
		String a_name = null;
		if (m_c == '=') {
			//[@を含む式]の前の情報で一度検索する。
			t_pat0 = Pattern.compile("^\\(.*xpath\\s*=\\s*\"([^\"]+)\\[@([\\w\\-]+)$");//jQのラムダ式に含まれるパターン:最初が(
			t_pat1 = Pattern.compile("\"xpath\":\"([^\"]+)\\[@([\\w\\-]+)$");//xpathの中のパターン
			t_pat2 = Pattern.compile("^([^\"]+)\\[@([\\w\\-]+)$");//通常の検索モードのパターン

			Matcher t_m = t_pat0.matcher(m_src);
			if (t_m.find()) {
				String cpath = t_m.group(1);
				a_name = t_m.group(2);
				return pickAttrsFromHtml(cpath, a_name);
			} else {
				t_m = t_pat1.matcher(m_src);
				if (t_m.find()) {
					String cpath = t_m.group(1);
					a_name = t_m.group(2);
					return pickAttrsFromHtml(cpath, a_name);
				} else {
					//先頭から検索のためにタグ名などを入力している場合
					t_m = t_pat2.matcher(m_src);
					if (t_m.find()) {
						String cpath = t_m.group(1);
						a_name = t_m.group(2);
						return pickAttrsFromHtml(cpath, a_name);
					}
				}
			}
		} else if (m_c == '@') {
			//[@を含む式]の前の情報で一度検索する。
			t_pat0 = Pattern.compile("^\\(.*xpath\\s*=\\s*\"(.+\\]?)\\[.*?$");//jQのラムダ式に含まれるパターン:最初が(
			t_pat1 = Pattern.compile("\"xpath\":\"(.+\\]?)\\[.*?$");//xpathの中のパターン
			t_pat2 = Pattern.compile("^([^\"]+\\]?)\\[[^\\[\\]]*?$");//通常の検索モードのパターン

			Matcher t_m = t_pat0.matcher(m_src);
			if (t_m.find()) {
				//ラムダ式の途中から検索語を抽出する場合、エスケープされた文字を戻す。
				String cpath = Fson.restore(t_m.group(1));
				return pickAttrsFromHtml(cpath, null);
			} else {
				t_m = t_pat1.matcher(m_src);
				if (t_m.find()) {
					//JSON式の途中から検索語を抽出する場合、エスケープされた文字を戻す。
					String cpath = Fson.restore(t_m.group(1));
					return pickAttrsFromHtml(cpath, null);
				} else {
					//先頭から検索のためにタグ名などを入力している場合
					t_m = t_pat2.matcher(m_src);
					if (t_m.find()) {
						String cpath = Fson.restore(t_m.group(1));
						return pickAttrsFromHtml(cpath, null);
					}
				}
			}
		}
		return null;
	}
	
	//あまりにもxml解析が遅いので、このダイアログが開いている間は保持しておく。
	private Document m_xmldoc;
	
	//性能劣化回避のため、HTMLを一気に取得し、解析して、属性値リストを作る。
	private ArrayList<String> pickAttrsFromHtml(String xpath, String attrName) {
		//HTMLを取得し、一気に属性リストを抽出する。
		String t_html = m_driver.getPageSource();
		if (xpath.startsWith("//")) {
			//頭をトル。
			xpath = xpath.substring(2);
		}
		if (xpath.matches("^[\\w\\-]+?::.*$")) {
			xpath = xpath.replaceFirst("^[\\w\\-]+?::", "");
		}
		
		String e_msg = "Xpath=" + xpath + " not found.";
		
		try {
			if (m_xmldoc == null) {
				DocumentBuilderFactory t_fact = DocumentBuilderFactory.newInstance();
				DocumentBuilder t_builder = t_fact.newDocumentBuilder();
				try {
					m_xmldoc = t_builder.parse(new ByteArrayInputStream(t_html.getBytes("UTF-8")));
				} catch (SAXException e) {
					//Tool.alertMSG(null, "Need to translate HTML to XML. It will takes time.");
					//HTMLはXMLでないので、エラーの際には、一回、変換してみる。
					t_html = VCentral.html2XML(t_html);
					m_xmldoc = t_builder.parse(new ByteArrayInputStream(t_html.getBytes("UTF-8")));
				}
			}
			XPath t_xpath =  XPathFactory.newInstance().newXPath();
			e_msg = "Xpath.evaluate error: //" + xpath;

			//属性を指定して情報（属性値）の塊を取り出す。
			NodeList n_list = null;
			if (attrName != null && attrName.length() > 0) {
				e_msg = "Xpath=" + "//" + xpath + "[@" + attrName + "]/@" + attrName + " not found.";
				
				//属性名指定の場合、属性値の候補はほぼ十分に絞り込まれると期待できる。
				n_list = (NodeList)t_xpath.evaluate("//" + xpath + "[@" + attrName + "]/@" + attrName, m_xmldoc, XPathConstants.NODESET);
			} else {
				e_msg = "Xpath=" + "//" + xpath + "[@*]/@*" + " not found.";

				//一つのタグ名のみ指定されていると、絞り込みが不十分な可能性が高い。属性を明にもつ条件をつけると改善できる。
					n_list = (NodeList)t_xpath.evaluate("//" + xpath + "[@*]/@*", m_xmldoc, XPathConstants.NODESET);
			}

			ArrayList<String> t_list = new ArrayList<String>();
			for (int i = 0; i < n_list.getLength(); i++) {
				Object t_elm = n_list.item(i);
				
				// class=\"属性値 属性値 属性値 属性値...\"。区切り記号は「;」もありうる。
				String a_value = t_elm.toString();
				String[] str_values = null;
				if (attrName != null) {
					a_value = a_value.replaceFirst("^" + attrName + "=\"(.*)\"$", "$1");
					if ("style".equals(attrName)) {//style属性は；を区切りにする。
						str_values = a_value.split("\\s*;\\s*", -1);
					} else {
						str_values = a_value.split("\\s+", -1);
					}
				} else {
					//属性名リストを作る
					a_value = a_value.replaceFirst("^([^\"]+)=.*$", "$1");
					str_values = new String[] {a_value};
				}
				

				String t_tail = "";
				if (attrName != null && str_values.length > 1) {
					//属性値を複数もつ属性名は区別のため、末尾に属性名を追加する。
					t_tail = "\t   :" + attrName;
				}
				for (int j = 0; j < str_values.length; j ++) {
					if (!t_list.contains(str_values[j])) {
						String t_val = str_values[j];
						if (t_val.length() > 0) {
							//新たな意味のある属性値が見つかったら追加する。
							t_list.add(t_val + t_tail);
						}
					}
				}
			}
			if (t_list.size() > 0) {
				Collections.sort(t_list);
				return t_list;
			}
		} catch (ParserConfigurationException | SAXException | IOException | XPathExpressionException e) {
			e.printStackTrace();
			Tool.alertMSG(null, e_msg + e.getMessage() + " in pickAttrsFromHtml.\nhtml =\n" + t_html);
		}
		return null;
	}

	private void initMenu() {
		//caretの前の文字列の取得。
		ArrayList<String> t_list = getAttrListForXPATH();
		
		if (t_list != null && t_list.size() > 0) {
			for (int i = 0 ; i < t_list.size(); i++) {
				if (!m_model.contains(t_list.get(i))) {
					m_model.addElement(t_list.get(i));
				}
			}
		}
	}
}
