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

import java.awt.BorderLayout;
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.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map.Entry;

import javassist.Modifier;
import javassist.NotFoundException;

import javax.swing.DefaultListModel;
import javax.swing.JDialog;
import javax.swing.JList;
import javax.swing.JScrollPane;

import com.ftinc.si.assist.test.ClassOutLine;
import com.ftinc.si.assist.test.CodeProcessor;
import com.ftinc.si.assist.test.FakeMethodRecord;
import com.ftinc.si.assist.test.Fson.NotSupportedClassException;
import com.ftinc.si.assist.test.Tool;

public class CodingAssistMenu extends JDialog {
	private JList<String> _list;
	private ArrayList<ArrayList<String>> m_stack;//キー絞り込みの際に、以前の状態を保持する。
	private String m_head = "";//キー入力による絞り込みキーワード
	
	private DefaultListModel<String> m_model;
	private FakeMethodRecord m_fake;
	private HashMap<String, Class<?>[]> m_excmap;
	private String m_filter;//返却型による絞り込み
	
	public String m_catches = null;//catch節
	public String m_target;//tryの中
	
	//選択した文字列
	public String m_answer = null;
	
	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));
		}
	}
	
	
	//frecは現在編集中のFakeMethod。nullの場合はAssertEditorから呼ばれている。srcは編集中のソース。
	//caretposは、元のTextAreaのカレット位置。class_listは、元のエディタが把握している変数名とクラスのMap。ptは表示すべき位置。
	public CodingAssistMenu(JDialog owner, FakeMethodRecord frec, String src, int caretpos, HashMap<String, Class<?>> class_list, Point pt, String filter) {
		super(owner, true);
		
		m_fake = frec;
		m_filter = filter;

		m_stack = new ArrayList<ArrayList<String>>();
		m_excmap = new HashMap<String, Class<?>[]>();
		
		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);
					String[] temp = m_model.getElementAt(i).toString().split(" :");
					m_answer = temp[0];
					makeCatches(m_excmap.get(m_model.getElementAt(i).toString()));
				}
			}
			@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) {
						String[] temp = m_model.getElementAt(i).toString().split(" :");
						m_answer = temp[0];
					}
					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_stack.size() == 0) {
						//何もないので終わる。
						setVisible(false);
					} else {
						//一つ前の状態にする。
						ArrayList<String> t_list = m_stack.get(m_stack.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_stack.remove(t_list);
					}
				} else {
					String t_char = Character.toString(e.getKeyChar());
					if (t_char.matches("[a-zA-Z_]")) {
						//文字の場合、絞り込む。
						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_stack.add(t_list);
						
						//空になるということは、そのまま入力であるので、絞り込みキーワードが答えである。メニューを閉じる。
						if (m_model.getSize() == 0) {
							m_answer = m_head;
							setVisible(false);
						}
					}
					
				}
			}
		});
		scrollPane.setViewportView(_list);
		if (src != null) {
			initMenu(src, caretpos, class_list);
		}
	}

	//辞書のキーになりそうな文字列を切り出す。
	private String findVarName(String src) {
		String t_src = src;
		if (!src.matches("^[\\s\\S]*\\)$")) {
			//最後が括弧でないなら。
			ArrayList<String> t_list = CodeProcessor.escDQStrings(src, "@");
			if (t_list.get(0).contains("\"")) {
				//切り取った文字列はダブルクォートをエスケープできない。つまり、文字列中で.を押した可能性大。
				return "";
			}
			//ピリオド含む連続文字列を返す。
			return t_src.replaceFirst("^[\\s\\S]*[,\\{;\\s\\+\\*\\(\\-/=]([\\w\\.\\$#]+)$", "$1");//[]のパターンが最長の連続文字列を切り出す。
		} else {
			//演算にかかわる余計な空白をとる。
			t_src = t_src.replaceAll("\\s*([\\)\\(\\+\\-,\\*/])\\s*", "$1");
			
			int _level = 0;//まず、最後の文字が)で終わっていることが分かっている。
						//()が来るたびに_levelを上げ下げする。再び0になったら

			int len = t_src.length();
			for (int i = 1; i < len + 1; i++) {
				if (i == len && _level == 0) {
					//最後まで来ていて、括弧がそろったとき。
					return t_src;
				}
				String c_str = t_src.substring(len - i,len - i + 1); 
				if (_level == 0 && c_str.matches("[=\\s\\-\\+\\*/,;\\{\\(]")) {
					//括弧がそろって、区切り文字に来た時。
					return t_src.substring(len - i + 1);
				}
				if (c_str.equals(")")) {
					_level++;
				} else if (c_str.equals("(")) {
					_level--;
				}
			}
		}
		return "";
	}

	private void initMenu(String src, int caretpos, HashMap<String, Class<?>> class_list) {
		//caretの前の文字列の取得。
		String t_str = src.substring(0, caretpos);
		//オブジェクトの塊を見つける。
		t_str = findVarName(t_str);
		
		if (t_str.length() == 0) {
			return;//オブジェクト名ではありえない。
		}
		//直前の一連の命令文。
		m_target = t_str;
		
		try {
			//_importListの作成
			src = CodeProcessor.preProcess4Import(src);
			
			//それ以外の変数名とクラスの対応表
			HashMap<String, Class<?>> t_map;
			t_map = CodeProcessor.analyzeVarToClass(new StringBuilder(src), class_list);
			if (CodeProcessor._importList.containsKey(t_str)) {
				//importされたクラスの後のピリオドの場合、FQNに変換する。
				t_str = CodeProcessor._importList.get(t_str);
			}

			Class<?> t_c = null;
			if (t_str.endsWith("]")) {
				//配列である。
				String cname = t_str.replaceFirst("\\[.*\\]", "");

				Class<?> t_c0 = t_map.get(cname);
				if (t_c0 == null) {
					//はずれの場合
					return;
				}
				//配列の要素のクラスが目的のクラス
				t_c = t_c0.getComponentType();
			} else {
				//配列型でない場合、素直にマップからクラスを特定する。
				t_c = t_map.get(t_str);
			}
			
			if (t_c == null) {
				if (t_str.contains("\"")) {
					//連続文字に"があったら変数ではない。
					return;
				}
				
				//オブジェクトではない。つまり、static呼び出しのようだ。
				//static呼び出しの場合、クラス名＋メソッド()となるので、オブジェクト変数として登録されていない。staticメソッドのみを選ぶ。
				t_c = Tool.forName(t_str);
				Method[] ms = t_c.getMethods();

				//メソッド名でソート
				Arrays.sort(ms, new Comparator<Method> () {
					@Override
					public int compare(Method m1, Method m2) {
						return m1.getName().compareTo(m2.getName());
					}
					
				});
				for (int i = 0; i < ms.length; i++) {
					if (!m_model.contains(ms[i].getName() + "()")) {
						if (Modifier.isStatic(ms[i].getModifiers())) {
							m_model.addElement(ms[i].getName() + getMethodDetail(ms[i]));
							makeExcps(ms[i]);
						}
					}
				}
			} else if (t_c.isArray()) {
				//配列は長さのみ返せる。
				m_model.addElement("length" + " : int");
			} else {
				//範囲はpublicで十分。オブジェクトとしては普通の権限。$nは全フィールドにアクセスできるので。
				Method[] ms = t_c.getMethods();
				
				//メソッド名でソート
				Arrays.sort(ms, new Comparator<Method> () {
					@Override
					public int compare(Method m1, Method m2) {
						return m1.getName().compareTo(m2.getName());
					}
					
				});
				for (int i = 0; i < ms.length; i++) {
					if (m_filter != null) {//ArgumentEditorのstackにオブジェクトのメソッド呼び出しがあった時、その返却値でフィルターする。
						if (!m_filter.equals(ms[i].getReturnType().getName())) {
							continue;//返却型のフィルターに合致しない場合、パスする。
						}
					}
					
					if (!m_model.contains(ms[i].getName() + getMethodDetail(ms[i]))) {
						m_model.addElement(ms[i].getName() + getMethodDetail(ms[i]));
						makeExcps(ms[i]);
					}
				}
				//obj.fieldの場合, 全Fieldを返す。メソッドがpublicだけなので。
				ClassOutLine c_line = Tool.getClassOutLine(t_c.getName());
				if (c_line != null) {
					HashMap<String, String> t_fs = c_line.getFields(m_fake);
					ArrayList<String> t_list = new ArrayList<String>();
					for(Entry<String, String> entry : t_fs.entrySet()) {
						t_list.add(entry.getKey() + " : " + entry.getValue());
					}
					//フィールド名でソート
					Collections.sort(t_list);
					for (int j = 0; j < t_list.size(); j++) {
						m_model.addElement(t_list.get(j));
					}
				}
			}
		} catch (NoSuchFieldException | NoSuchMethodException | NotFoundException | SecurityException | 
				ClassNotFoundException | InstantiationException | IllegalArgumentException | IllegalAccessException | 
				InvocationTargetException | ParseException | NotSupportedClassException e1) {
			//「.」の前が登録されたクラスとは限らないので無視。
			//e1.printStackTrace();
			Tool.logIfDebug(e1, "@CodingAssistMennu#initMenu");
		}
	}
	
	//メソッドのキーから例外の配列を作る。
	private void makeExcps(Method ms) {
		Class<?>[] t_exps = ms.getExceptionTypes();
		if (t_exps != null && t_exps.length > 0) {
			m_excmap.put(ms.getName() + getMethodDetail(ms), t_exps);
		}
	}
	
	//catch節を合成する。
	private void makeCatches(Class<?>[] exlist) {
		if (exlist != null) {
			String t_catch = ";\n";
			for (int i = 0; i < exlist.length; i++) {
				t_catch += "} catch (" + exlist[i].getName() + " e) { e.printStackTrace();\n";
			}
			t_catch += "}";
			m_catches = t_catch;
		}
	}

	
	//メソッドの特徴を表現する。引数と返却値の型の明示
	private String getMethodDetail(Method m) {
		String t_str = "(";
		String str_args = "";
		Class<?>[] t_args = m.getParameterTypes();
		
		for (int i = 0; i < t_args.length; i++) {
			if (m_filter != null) {
				str_args += t_args[i].getName();//ArgumentEditorへはFQNを渡す。
			} else {
				str_args += t_args[i].getSimpleName();
			}
			if (i < t_args.length - 1) {
				str_args += ",";
			}
		}
		Class<?> t_c = m.getReturnType();
		t_str += str_args + ") :" + t_c.getName();
		return t_str;
	}

}
