/*
 * 作成日: 2007/11/09
 */
package asandatabasebrowser.view;

import java.awt.Color;
import java.awt.Event;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Arrays;

import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.UndoManager;

import net.sourceforge.swingx.SxAction;
import net.sourceforge.swingx.SxMouseAdapter;
import net.sourceforge.swingx.SxTextUtilities;
import net.sourceforge.swingx.SxUtilities;
import asandatabasebrowser.Global;
import asandatabasebrowser.action.SqlExcludeCommentAction;
import asandatabasebrowser.action.SqlFormatAction;
import asandatabasebrowser.action.SqlUnformatAction;
import blanco.commons.sql.format.BlancoSqlConstants;
import blanco.commons.sql.format.BlancoSqlParser;
import blanco.commons.sql.format.BlancoSqlRule;
import blanco.commons.sql.format.BlancoSqlTokenConstants;
import blanco.commons.sql.format.valueobject.BlancoSqlToken;

public class SqlEditor extends JTextPane {
	static final int TAB_SIZE = 4;
    BlancoSqlParser fParser = new BlancoSqlParser();
    static SimpleAttributeSet PLANE = new SimpleAttributeSet();
    static SimpleAttributeSet KEYWORD = new SimpleAttributeSet();
    static SimpleAttributeSet COMMENT = new SimpleAttributeSet();
    static SimpleAttributeSet VALUE = new SimpleAttributeSet();
    static SimpleAttributeSet NAME = new SimpleAttributeSet();
    static SimpleAttributeSet SQL89 = new SimpleAttributeSet();
    static SimpleAttributeSet SQL92 = new SimpleAttributeSet();
    static SimpleAttributeSet SQL99 = new SimpleAttributeSet();
    static SimpleAttributeSet SQL_FAMOUS = new SimpleAttributeSet();
    static SimpleAttributeSet UNKNOWN = new SimpleAttributeSet();
    
    static {
    	// 色はだいたい BlancoSqlEditorPlugin に合わせてます。
        StyleConstants.setForeground(KEYWORD, Color.BLUE);
        StyleConstants.setForeground(COMMENT, new Color(0, 128, 0));	// dark green
        StyleConstants.setForeground(VALUE, Color.RED);
        StyleConstants.setBackground(UNKNOWN, Color.RED);  // 背景の色
        StyleConstants.setForeground(SQL89, new Color(255, 0, 255));
        StyleConstants.setForeground(SQL92, new Color(192, 0, 255));
        StyleConstants.setForeground(SQL99, new Color(96, 0, 255));
        StyleConstants.setForeground(SQL_FAMOUS, new Color(0, 128, 255));
    }
	BlancoSqlRule rule;
	UndoManager undoManager = new UndoManager();
	RedoAction redoAction;
	UndoAction undoAction;
    SqlFormatAction sqlFormatAction;
    SqlCommentAction sqlCommentAction;
    SqlCommentAddAction sqlCommentAddAction;
    SqlCommentDeleteAction sqlCommentDeleteAction;
    SqlUnformatAction sqlUnformatAction;
    SqlExcludeCommentAction sqlExcludeCommentAction;
    
    public SqlEditor(String sql, BlancoSqlRule rule, Font font) {
		super();
		this.rule = rule;
		this.setText(sql);
		this.setFont(font);
		this.selectAll();
		
		// アクションの設定
		ActionMap amap = getActionMap();
		redoAction = new RedoAction();
		undoAction = new UndoAction();
		sqlFormatAction = new SqlFormatAction(rule, SqlEditor.this, false);
		sqlCommentAction = new SqlCommentAction();
		sqlCommentAddAction = new SqlCommentAddAction();
		sqlCommentDeleteAction = new SqlCommentDeleteAction();
		sqlUnformatAction = new SqlUnformatAction(SqlEditor.this);
		sqlExcludeCommentAction = new SqlExcludeCommentAction(SqlEditor.this);
		
		amap.put(undoAction.getClass().getName(), undoAction);
		amap.put(redoAction.getClass().getName(), redoAction);
		amap.put(sqlFormatAction.getClass().getName(), sqlFormatAction);
		amap.put(sqlCommentAction.getClass().getName(), sqlCommentAction);
		amap.put(sqlCommentAddAction.getClass().getName(), sqlCommentAddAction);
		amap.put(sqlCommentDeleteAction.getClass().getName(), sqlCommentDeleteAction);
		amap.put(sqlUnformatAction.getClass().getName(), sqlUnformatAction);
		amap.put(sqlExcludeCommentAction.getClass().getName(), sqlExcludeCommentAction);

		
		InputMap imap = getInputMap();
		imap.put(undoAction.getAcceleratorKey(), undoAction);
		imap.put(redoAction.getAcceleratorKey(), redoAction);
		imap.put(sqlFormatAction.getAcceleratorKey(), sqlFormatAction);
		imap.put(sqlCommentAction.getAcceleratorKey(), sqlCommentAction);
		// デフォルトのアクションにアクセラレータキー、アイコンを登録する。
		Action act_cut = amap.get("cut-to-clipboard");
		if (act_cut.getValue(Action.ACCELERATOR_KEY) == null) {
			act_cut.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke('X', Event.CTRL_MASK));	// Ctrl+X
		}
		if (act_cut.getValue(Action.SMALL_ICON) == null) {
			act_cut.putValue(Action.SMALL_ICON, Global.getImageIcon("/resources/eclipse_EditCut.png"));
		}
		Action act_copy = amap.get("copy-to-clipboard");
		if (act_copy.getValue(Action.ACCELERATOR_KEY) == null) {
			act_copy.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke('C', Event.CTRL_MASK));	// Ctrl+C
		}
		if (act_copy.getValue(Action.SMALL_ICON) == null) {
			act_copy.putValue(Action.SMALL_ICON, Global.getImageIcon("/resources/eclipse_EditCopy.png"));
		}
		Action act_paste = amap.get("paste-from-clipboard");
		if (act_paste.getValue(Action.ACCELERATOR_KEY) == null) {
			act_paste.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke('V', Event.CTRL_MASK));	// Ctrl+V
		}
		if (act_paste.getValue(Action.SMALL_ICON) == null) {
			act_paste.putValue(Action.SMALL_ICON, Global.getImageIcon("/resources/eclipse_EditPaste.png"));
		}
		Action act_selall = amap.get("select-all");
		if (act_selall.getValue(Action.ACCELERATOR_KEY) == null) {
			act_selall.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke('A', Event.CTRL_MASK));	// Ctrl+A
		}
		
		getDocument().addUndoableEditListener(new UndoableListener());

		// ポップアップメニューを登録
		SxUtilities.addPopupMenuMouseListener(this);
		this.addMouseListener(new PopupListener());
		
        StyledDocument doc = (StyledDocument) this.getDocument();
        invokeSetHighlight(doc);
        doc.addDocumentListener(new DocListener());
    }
    
	/** 強調表示を設定する. */
	void setHighlight(StyledDocument doc) {
		// SQL文を取り出す。
     	String text = "";
		try {
			text = doc.getText(0, doc.getLength());
		} catch (BadLocationException e) {
			e.printStackTrace();
			return;	// フツー、ココには来ないけれど、来るようならば色づけなどしない。
		}
		// SQL文を字句解析し、字句ごとにぶった切る。
		ArrayList tokens = null;
		try {
			tokens = fParser.parse(text);
		} catch (RuntimeException e) {
			//e.printStackTrace();
			return;	// 不完全なSQLだと起こりえる。
		}
        // 一旦、すべての属性を消す
        doc.setCharacterAttributes(0, text.length(), PLANE, true);
        // トークンごとに色分けする。
        for (int i=0; i<tokens.size(); i++) {
			BlancoSqlToken t = (BlancoSqlToken) tokens.get(i);
			int pos = t.getPos();
			String str = t.getString();
			switch (t.getType()) {
			case BlancoSqlTokenConstants.SPACE:		// 空白、TAB、改行など
				break;
			case BlancoSqlTokenConstants.SYMBOL:	// 記号. " <="のような２つで１つの記号もある。
				break;
			case BlancoSqlTokenConstants.KEYWORD:	// キーワード. "SELECT", "ORDER"など.
				str = str.toUpperCase();
                if (Arrays.binarySearch(BlancoSqlConstants.SQL89_RESERVED_WORDS, str) >= 0) {
					doc.setCharacterAttributes(pos, str.length(), SQL89, true);
				} else if (Arrays.binarySearch(BlancoSqlConstants.SQL92_RESERVED_WORDS, str) >= 0) {
					doc.setCharacterAttributes(pos, str.length(), SQL92, true);
				} else if (Arrays.binarySearch(BlancoSqlConstants.SQL99_RESERVED_WORDS, str) >= 0) {
					doc.setCharacterAttributes(pos, str.length(), SQL99, true);
				} else if (Arrays.binarySearch(BlancoSqlConstants.SQL_FAMOUS_WORDS, str) >= 0) {
					doc.setCharacterAttributes(pos, str.length(), SQL_FAMOUS, true);
				} else {
					doc.setCharacterAttributes(pos, str.length(), KEYWORD, true);
					// ココには来ないはず
					System.out.println("keyword="+str);
				}
				break;
			case BlancoSqlTokenConstants.NAME:		// 名前.テーブル名、列名など。ダブルクォーテーションが付く場合がある。
                doc.setCharacterAttributes(pos, str.length(), NAME, true);
				break;
			case BlancoSqlTokenConstants.VALUE:		// 値. 数値（整数、実数）、文字列など。
                doc.setCharacterAttributes(pos, str.length(), VALUE, true);
				break;
			case BlancoSqlTokenConstants.COMMENT:	// コメント. シングルラインコメントとマルチラインコメントがある。
                doc.setCharacterAttributes(pos, str.length(), COMMENT, true);
				break;
			case BlancoSqlTokenConstants.END:		// SQL文の終わり.
				break;
			case BlancoSqlTokenConstants.UNKNOWN:	// 解析不可能なトークン. 通常のSQLではありえない。
                doc.setCharacterAttributes(pos, str.length(), UNKNOWN, true);
				break;
			default:
				assert false: t.getType();	// ありえない
			}
		}
    }

	void invokeSetHighlight(final StyledDocument doc) {
        // ドキュメント変更中に呼ぶと競合するので、処理後に呼ぶようにする。
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                setHighlight(doc);
            }
        });
    }

	private class PopupListener extends SxMouseAdapter {
		public void mousePopup(MouseEvent ev) {
			JPopupMenu popup = new JPopupMenu();
			if (System.getProperty("debug") != null) {
				// TODO:強調表示を含めたUndo/Redoがうまくいかない。
				popup.add(undoAction);
				popup.add(redoAction);
				popup.addSeparator();
			}
			SxUtilities.addMenuItems(SqlEditor.this, popup);
			popup.addSeparator();
//			popup.add(sqlCommentAction);
			popup.add(sqlCommentAddAction);
			popup.add(sqlCommentDeleteAction);
			popup.addSeparator();
			popup.add(sqlFormatAction);
			popup.addSeparator();
			popup.add(sqlUnformatAction);
			popup.add(sqlExcludeCommentAction);
			if (System.getProperty("debug") != null) {
				popup.addSeparator();
				popup.add(new SqlFormatAction(rule, SqlEditor.this, true));
			}
			popup.show(SqlEditor.this, ev.getX(), ev.getY());
		}
		private JMenuItem createMenuItem(Action action, String text,
				int mnemonic, boolean enable) {
			assert action != null;
			assert text != null;

			JMenuItem menu = new JMenuItem(action);
			menu.setText(text);
			menu.setMnemonic(mnemonic);
			menu.setEnabled(enable);
			KeyStroke ks = (KeyStroke) action.getValue(Action.ACCELERATOR_KEY);
			if (ks != null) {
				menu.setAccelerator(ks);
			}
			return menu;
		}
	}
	
	/** ドキュメントが変更されたときのリスナー. */
    private class DocListener implements DocumentListener {
        public void insertUpdate(DocumentEvent e) {
        	System.out.println(getText());
        	System.out.println("insertUpdate(e)"+e);
            invokeSetHighlight((StyledDocument)e.getDocument());
        }
        public void removeUpdate(DocumentEvent e) {
            //System.out.println("removeUpdate(e)"+e);
            invokeSetHighlight((StyledDocument)e.getDocument());
        }
        public void changedUpdate(DocumentEvent e) {
            //System.out.println("changedUpdate(e)"+e);
            // 属性が変わったときは、何もしなくてよい。
        }
    }
    class UndoEdit extends AbstractUndoableEdit {
    	String before;
    	String after;
    	UndoEdit(String before, String after) {
    		this.before = before;
    		this.after = after;
    	}
    	public void undo() {
    		setText(before);
     	}
    	public void redo() {
    		setText(after);
    	}
    }
    /** SQLエディタで編集が行われたとき、その変更に対してUndoManagerに登録するかどうかを行う */
    private class UndoableListener implements UndoableEditListener {
		public void undoableEditHappened(UndoableEditEvent e) {
			System.out.println(e.getEdit().getClass());
			AbstractDocument.DefaultDocumentEvent event = (AbstractDocument.DefaultDocumentEvent) e.getEdit();
			if (event.getType() != DocumentEvent.EventType.CHANGE) {
				undoManager.addEdit(e.getEdit());
				undoAction.updateStatus();
				redoAction.updateStatus();
			}
		}
	}
	private class UndoAction extends SxAction {
		static final String TITLE = "元に戻す(U)";
		UndoAction() {
			super(TITLE);
    		putValue(MNEMONIC_KEY, new Integer(KeyEvent.VK_U));
    		putValue(SMALL_ICON, Global.undoIcon);
    		putValue(SHORT_DESCRIPTION, "元に戻す");
    		putValue(LONG_DESCRIPTION, "元に戻す");
			putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke('Z', Event.CTRL_MASK));
			updateStatus();
		}
		public void actionPerformed(ActionEvent e) {
			undoManager.undo();
			redoAction.updateStatus();
			updateStatus();
		}
		public void updateStatus() {
			boolean canUndo = undoManager.canUndo();
			if (canUndo) {
				setEnabled(true);
				putValue(Action.NAME, undoManager.getUndoPresentationName());
			} else {
				setEnabled(false);
				putValue(Action.NAME, TITLE);
			}
		}
	}
	private class RedoAction extends SxAction {
		final static String TITLE = "やり直し(R)";
		RedoAction() {
			super(TITLE);
    		putValue(MNEMONIC_KEY, new Integer(KeyEvent.VK_R));
    		putValue(SMALL_ICON, Global.redoIcon);
    		putValue(SHORT_DESCRIPTION, "やり直し");
    		putValue(LONG_DESCRIPTION, "やり直し");
			putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke('Y', Event.CTRL_MASK));
			updateStatus();
		}
		public void actionPerformed(ActionEvent e) {
			undoManager.redo();
			undoAction.updateStatus();
			updateStatus();
		}
		public void updateStatus() {
			boolean canRedo = undoManager.canRedo();
			if (canRedo) {
				setEnabled(true);
				putValue(Action.NAME, undoManager.getRedoPresentationName());
			} else {
				setEnabled(false);
				putValue(Action.NAME, TITLE);
			}
		}
	}

    class SqlCommentAction extends SxAction {
    	SqlCommentAction() {
    		super("コメントの切り替え(L)");
    		putValue(MNEMONIC_KEY, new Integer('L'));
    		putValue(SMALL_ICON, Global.getImageIcon("/resources/CommentAdd.png"));
    		putValue(SHORT_DESCRIPTION, "コメントの切り替え");
    		putValue(LONG_DESCRIPTION, "コメントの切り替え");
    		putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke('-', Event.CTRL_MASK)); // Ctrl+’−’
    	}
    	public void actionPerformed(ActionEvent event) {
    		// 選択された範囲が何行目から何行目かを調べる。
    		String text = getText();
    		int start = getSelectionStart();
    		int end = getSelectionEnd();
    		int[] slinecol = SxTextUtilities.getLineColumnIndex(text, start);
    		int[] elinecol = SxTextUtilities.getLineColumnIndex(text, end);
    		int sline = slinecol[0];
    		int eline = elinecol[0];
    		if (elinecol[1] == 0 && sline < eline) eline--;
    		System.out.println("sline="+sline+" eline="+eline);
    		// テキストを行ごとの配列にする。
    		String[] lines = SxTextUtilities.splitLine(text);
    		// すべての行の先頭がコメントから始まっているか？
    		boolean allComment = true;
    		for (int i=sline; i<=eline; i++) {
    			if (! lines[i].startsWith("--")) {
    				allComment = false;
    				break;
    			}
    		}
    		// すべての行がコメントから始まっている場合は、コメントを外す。
    		if (allComment) {
        		for (int i=sline; i<=eline; i++) {
        			lines[i] = lines[i].substring(2);
        		}
    		} else {
        		for (int i=sline; i<=eline; i++) {
        			lines[i] = "--" + lines[i];
        		}
        	}
    		// 分けた行をつなげる。
    		text = "";
    		for (int i=0; i<lines.length; i++) {
    			text += lines[i];
    		}
    		setText(text);
    	}
    	public void updateStatus() {}
    }
    class SqlCommentAddAction extends SxAction {
    	SqlCommentAddAction() {
    		super("コメントの追加");
    		putValue(SHORT_DESCRIPTION, "コメントの追加");
    		putValue(LONG_DESCRIPTION, "コメントの追加");
    		putValue(SMALL_ICON, Global.getImageIcon("/resources/CommentAdd.png"));
    	}
    	public void actionPerformed(ActionEvent event) {
    		// 選択された範囲が何行目から何行目かを調べる。
    		String text = getText();
    		int start = getSelectionStart();
    		int end = getSelectionEnd();
    		int[] slinecol = SxTextUtilities.getLineColumnIndex(text, start);
    		int[] elinecol = SxTextUtilities.getLineColumnIndex(text, end);
    		int sline = slinecol[0];
    		int eline = elinecol[0];
    		if (elinecol[1] == 0 && sline < eline) eline--;
    		System.out.println("sline="+sline+" eline="+eline);
    		// テキストを行ごとの配列にする。
    		String[] lines = SxTextUtilities.splitLine(text);
    		// コメントを追加
    		for (int i=sline; i<=eline; i++) {
    			lines[i] = "--" + lines[i];
    		}
    		// 分けた行をつなげる。
    		text = "";
    		for (int i=0; i<lines.length; i++) {
    			text += lines[i];
    		}
    		setText(text);
    	}
    	public void updateStatus() {}
    }
    class SqlCommentDeleteAction extends SxAction {
    	SqlCommentDeleteAction() {
    		super("コメントの除去");
    		putValue(SHORT_DESCRIPTION, "コメントの除去");
    		putValue(LONG_DESCRIPTION, "コメントの除去");
    		putValue(SMALL_ICON, Global.getImageIcon("/resources/CommentDelete.png"));
    	}
    	public void actionPerformed(ActionEvent event) {
    		// 選択された範囲が何行目から何行目かを調べる。
    		String text = getText();
    		int start = getSelectionStart();
    		int end = getSelectionEnd();
    		int[] slinecol = SxTextUtilities.getLineColumnIndex(text, start);
    		int[] elinecol = SxTextUtilities.getLineColumnIndex(text, end);
    		int sline = slinecol[0];
    		int eline = elinecol[0];
    		if (elinecol[1] == 0 && sline < eline) eline--;
    		System.out.println("sline="+sline+" eline="+eline);
    		// テキストを行ごとの配列にする。
    		String[] lines = SxTextUtilities.splitLine(text);
    		// すべての行がコメントから始まっている場合は、コメントを外す。
    		for (int i=sline; i<=eline; i++) {
    			if (lines[i].startsWith("--")) {
    				lines[i] = lines[i].substring(2);
    			}
    		}
    		// 分けた行をつなげる。
    		text = "";
    		for (int i=0; i<lines.length; i++) {
    			text += lines[i];
    		}
    		setText(text);
    	}
    	public void updateStatus() {}
    }
    
    public static void main(String[] args) {
    	SwingUtilities.invokeLater(new Runnable() {
            public void run() {
        		// スッキリとしたフォントにする。
        		System.setProperty("swing.plaf.metal.controlFont", "Dialog-12");
            	JFrame frame = new JFrame("SQL Editor");
            	BlancoSqlRule rule = new BlancoSqlRule();
            	String sql = 
            		"-- サンプルです☆\n"+
            		"UPDATE\n"+
            		"table1\n"+
            		"SET\n"+
            		"col2 = 'ABC'\n"+
            		",col3 = 123\n"+
            		"WHERE\n"+
            		"col1 = 5;\n"+
            		"INSERT INTO table2 values(1,2,3);";
            	Font font = new Font("Monospaced", Font.PLAIN, 12);
            	SqlEditor editor = new SqlEditor(sql, rule, font);
            	editor.setBorder(new EmptyBorder(5, 10, 0, 0));
            	frame.getContentPane().add(new JScrollPane(editor));
            	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            	frame.setSize(640, 800);
            	frame.setVisible(true);
            }
        });    	
    }
}
