package jnyqide;

//
//  CodePane.java
//  nyquist
//
//  Created by Roger Dannenberg on 12/15/07.
//  Copyright 2007 Roger B. Dannenberg. All rights reserved.
//

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.undo.*;

// CodePane subclasses a JScrollPane and initializes it to contain
// a JTextPane which holds a DefaultStyleDocument. Syntax coloring
// and paren balancing is performed.
//
// a CodePane also has a Timer -- it is used to blink a character somewhere.
//
public class CodePane extends JScrollPane implements DocumentListener,
		CaretListener, AdjustmentListener, KeyListener {
	public JTextPane pane;
	public DefaultStyledDocument doc;
	public Timer timer;
	public boolean evenParens; // used by MainFrame for command input
	// (when user types return, if parens are balanced, that is, evenParens
	// is true, then the command is sent to Nyquist)
	// when user types a close paren, the matching one is blinked, and
	// when user moves cursor, the matching paren is highlighted in bold
	public int blinkLoc; // where to blink a matching paren
	public int highlightLoc; // where to highlight a matching paren
	public boolean blinkOn;
	// mainFrame is a handle to access some methods, but it is also a
	// flag to tell us if this is a command entry window:
	public MainFrame mainFrame;
	public boolean isSal;
	int caretLine; // holds current line number
	int caretColumn; // holds current column
	int fontSize; // the font size
	JLabel statusBar;
    boolean indentedByNewline;
	
	Font mainFont;

	final int TIMER_DELAY = 1000; // ms

	public CodePane(Dimension preferredSize, MainFrame mf, JLabel sb, int fontSz) {
		super();
		blinkLoc = 0;
		blinkOn = false; // initialize
		fontSize = fontSz;
		mainFrame = mf;
		statusBar = sb;
        indentedByNewline = false;
		isSal = false;
		doc = new DefaultStyledDocument();
		pane = new JTextPane(doc);
		mainFont = new Font("sdfi", Font.PLAIN, 11);
		if (mainFont == null) {
			mainFont = new Font("Lucida Console", Font.PLAIN, 11);
		}
		pane.setFont(mainFont);
		
		
		getViewport().add(pane);
		setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
		setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
		
		

		// create timer for blinking
		ActionListener blinkOffTask = new ActionListener() {
			public void actionPerformed(ActionEvent evt) {
				timer.stop(); // timer is just a one-shot, not periodic
				blinkOn = false;
				synchronousUpdate();
			}
		};
		timer = new Timer(TIMER_DELAY, blinkOffTask);

		doc.addDocumentListener(this);
		pane.addCaretListener(this);
		this.getHorizontalScrollBar().addAdjustmentListener(this);
		this.getVerticalScrollBar().addAdjustmentListener(this);

		final UndoManager undo = new UndoManager();
		doc.addUndoableEditListener(new UndoableEditListener() {
			public void undoableEditHappened(UndoableEditEvent evt) {
				// do not make style changes undoable -- these
				// are fixed automatically
				if (!evt.getEdit().getPresentationName().equals("style change")) {
					undo.addEdit(evt.getEdit());
				}
			}
		});

		// Create an undo action and add it to the text component
		pane.getActionMap().put("Undo", new AbstractAction("Undo") {
			public void actionPerformed(ActionEvent evt) {
				try {
					if (undo.canUndo()) {
						undo.undo();
					}
				} catch (CannotUndoException e) {
				}
			}
		});

		// Bind the undo action to ctl-Z
		pane.getInputMap().put(KeyStroke.getKeyStroke("control Z"), "Undo");

		// Create a redo action and add it to the text component
		pane.getActionMap().put("Redo", new AbstractAction("Redo") {
			public void actionPerformed(ActionEvent evt) {
				try {
					if (undo.canRedo()) {
						undo.redo();
					}
				} catch (CannotRedoException e) {
				}
			}
		});

		// Bind the redo action to ctl-Y
		pane.getInputMap().put(KeyStroke.getKeyStroke("control Y"), "Redo");

		// add self as a key listener
		pane.addKeyListener(this);

		if (preferredSize != null)
			setPreferredSize(preferredSize);

	}

	public void updateCaretLoc() {
		String text;
		try {
			text = pane.getText();
		} catch (Exception ex) {
			System.out.println("exception in updateCaretLoc");
			return;
		}
		int pos = pane.getCaretPosition();
		caretColumn = 0;
		caretLine = 0;
		int i = 0;
		while (i++ < pos) {
			if (text.charAt(i - 1) == '\n') {
				caretColumn = 0;
				caretLine++;
			} else {
				caretColumn++;
			}
		}
		// the first line is 1, not 0
		statusBar.setText(Integer.toString(caretLine + 1) + ":"
				+ Integer.toString(caretColumn));
	}

	public void changedUpdate(DocumentEvent e) {
		// System.out.println("changedUpdate " + e);
	}

	public void removeUpdate(DocumentEvent e) {
		synchronousUpdate();
	}

	public void insertUpdate(DocumentEvent e) {
		// System.out.println("insertUpdate calls thread.update");
		synchronousUpdate();
	}

	public void caretUpdate(CaretEvent e) {
		// System.out.println("caretUpdate " + e.getDot() + " mark " +
		// e.getMark());
		if (e.getDot() == e.getMark()) {
			highlightMatchingParen(e.getDot() - 1);
			updateCaretLoc();
			// System.out.println("caretUpdate: " + e.getDot() + " is line " +
			// caretLine + " col " + caretColumn);
		} else {
			highlightMatchingParen(-1);
		}
	}

	public void adjustmentValueChanged(AdjustmentEvent evt) {
		// System.out.println("adjustmentValueChanged");
		synchronousUpdate();
	}

	// This method can be invoked from any thread. It
	// invokes the setText and modelToView methods, which
	// must run in the event dispatching thread. We use
	// invokeLater to schedule the code for execution
	// in the event dispatching thread.
	protected void highlightMatchingParen(final int pos) {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				if (pos == -1) {
					highlightLoc = pos;
				} else if (charAtPos(pos) == ')') {
					try {
						highlightLoc = findOpenParen(pane.getText(0, pos), pos);
					} catch (BadLocationException e) {
						System.out.println(e);
						return;
					}
					if (highlightLoc == pos)
						highlightLoc = -1;
				} else { // not at close paren, so turn off highlight
					highlightLoc = -1;
				}
				synchronousUpdate();
			}
		});
	}

	char charAtPos(int pos) {
		String text;
		try {
			text = doc.getText(pos, 1);
		} catch (BadLocationException e) {
			return 0;
		}
		return text.charAt(0);
	}

	// complete the KeyListener interface:
	public void keyReleased(KeyEvent ke) {
		return;
	}

	public void keyPressed(KeyEvent ke) {
		return;
	}

	public void keyTyped(KeyEvent ke) {
		// System.out.println("CodePane keyTyped |" + ke.getKeyChar() + "|");
		// cases:
		// newline
		// mainFrame
		// evenParens: sendCommandToNyquist
		// !evenParens:
		// LISP: print "paren mismatch"
		// indent handling
		// else
		// indent handling
		// otherwise
		// process key
		//
		// this is where we put auto parentheses:
		char ch = ke.getKeyChar();
		int pos = pane.getCaretPosition();
        // backspace can take out indentation immediately after newline that
        // generates indentation, but not otherwise, so clear indentedByNewline
        // as soon as something is typed (other than backspace):
        if (ch != '\b') {
            indentedByNewline = false;
        }
		if (ch == '\n') {
			// insertIndentation(p);
			if (mainFrame != null) { // if this is the command entry window
				if (evenParens) {
					if (doc.getLength() == pos) {
						// only do indentation if newline is at end of text
						insertIndentation(pos);
						// remove newline
						pos = doc.getLength(); // take out newline at end
						int newlinePos = pos - 1;
						try {
							while (!doc.getText(newlinePos, 1).equals("\n")) {
								newlinePos--;
							}
						} catch (BadLocationException e) {
							System.out.println(e);
							return;
						}
						pane.setSelectionStart(newlinePos);
					} else { // no indentation, just remove newline
						pane.setSelectionStart(pos - 1);
					}
					pane.setSelectionEnd(pos);
					pane.replaceSelection("");
					mainFrame.sendCommandToNyquist();
				} else {
					if (!isSal) {
						mainFrame.jOutputArea
								.append("Invalid command - paren mismatch\n");
					}
					insertIndentation(pos);
				}
			} else { // not the command entry window
				if (insertIndentation(pos) > 1) {
                    indentedByNewline = true; // if the next char is backspace, all
                    // the indentation will be removed at once
                } // otherwise a backspace will either delete one space or one newline
                // so we do not want the indentedByNewline flag set to enable deleting
                // more spaces
			}
		} else if (ch == '\t') {
			int spaces = TextColor.INDENT - (caretColumn - 1)
					% TextColor.INDENT;
			// remove tab and replace with spaces
			pane.setSelectionStart(pos - 1);
			pane.setSelectionEnd(pos);
			pane.replaceSelection("        ".substring(0, spaces));
        } else if (ch == '\b' && indentedByNewline) {
            indentedByNewline = false; // don't do this until another newline
            // clear all the way back to beginning of line in this case
            int newlinePos = pos - 1;
            try {
                while ((newlinePos >= 0) && !doc.getText(newlinePos, 1).equals("\n")) {
                    newlinePos -= 1;
                }
                // now newlinePos is the position of the typed newline. Delete from
                // after newline to current position
                pane.setSelectionStart(newlinePos + 1);
                pane.setSelectionEnd(pos);
                pane.replaceSelection("");
            } catch (BadLocationException e) {
                System.out.println(e);
                return;
            }
		} else { // ordinary key handling
			if (ke.getKeyChar() == ')') {
				// get caret location
				blinkParen(pos);
			} else if (ch == '(') {
				if (MainFrame.prefParenAutoInsert) {
					pane.replaceSelection(")");
					pane.setCaretPosition(pos);
				}
			}
			String text;
			int start = Math.max(pos - 100, 0);
			try {
				text = pane.getText(start, pos - start);
			} catch (Exception ex) {
				System.out.println("exception in keyTyped: start " + start
						+ " pos " + pos + " pane " + pane);
				return;
			}
			// simulate typing the character unless it's a backspace
			if (ch != '\b')
				text += ch;
			else
				pos--;

			String identifier;
			int identLoc;
			boolean forceExact = false; // force an exact match to complete word
			if (isSal) { // generate completion list for SAL
				// delimiters are "{}(),[]\n #\""
				// look back for identifier immediately before pos
				// if none found, look back for unbalanced paren, then search
				// back from there
				identifier = getSalIdentifier(text, pos - start);
				int len = identifier.length();
				identLoc = pos - len;
				forceExact = (len > 0 && identifier.charAt(len - 1) == ' ');
				if (len == 0) { // not found
					int openParenLoc = findOpenParen(text, pos - start);
					identifier = getSalIdentifier(text, openParenLoc);
					identLoc = start + openParenLoc - identifier.length();
					forceExact = true;
				}
			} else { // generate completion list for Lisp
				// look back for unmatched open paren, then forward for
				// identifier
				// look back a maximum of 100 characters
				int openParenLoc = findOpenParen(text, pos - start);
				openParenLoc++;
				identifier = getIdentifier(text, openParenLoc);
				identLoc = start + openParenLoc;
				int len = identifier.length();
				forceExact = (len > 0 && identifier.charAt(len - 1) == ' ');
			}
			// System.out.println("keyTyped identifier is: |" + identifier +
			// "|");
			// put up words list
			WordList.printList(identifier, pane, identLoc, pos + 1, forceExact,
					isSal);
		}
	}

	// get an identifier starting at pos -- if a complete identifier (terminated
	// by something)
	// is found, the identifier is terminated by a space character
	static String getIdentifier(String text, int pos) {
		int idEnd = pos; // search forward to find identifier
		String lispIdChars = "~!@$%^&*-_+={}|:<>?/";
		if (text.length() == 0 || pos < 0)
			return text; // special cases
		while (true) {
			if (idEnd == text.length()) {
				text = text.substring(pos); // still typing
				break;
			}
			char idChar = text.charAt(idEnd);
			if (!Character.isLetterOrDigit(idChar)
					&& lispIdChars.indexOf(idChar) == -1) {
				text = text.substring(pos, idEnd) + " "; // complete
				break; // idEnd is one past last character
			}
			idEnd++;
		}
		return text;
	}

	static int findColumnOf(String text, int pos) {
		int col = 0;
		pos--;
		while (pos >= 0 && text.charAt(pos) != '\n') {
			col++;
			pos--;
		}
		return col;
	}

    // returns how many spaces we indented
	private int insertIndentation(int p) {
		String text;
		int desired = 0; // desired indentation of the previous line
		// initialized because compiler can't figure out that it's
		// initialized below before it is used
		try {
			text = pane.getText(0, p);
		} catch (Exception e) {
			System.out.println("exception in insertIndentation");
			return 0;
		}
		int indent;
		if (isSal) {
			indent = salIndentAmount(p);
			desired = TextColor.indentBefore;
		} else {
			indent = autoIndentAmount(text, p);
		}
		String indentation = "";
        for (int i = 0; i < indent; i++) {
			indentation += " ";
		}
		// System.out.println("before replaceSelection(indentation)");
		pane.replaceSelection(indentation);
		// System.out.println("after replaceSelection(indentation)");
		if (isSal) { // indent the previous line as well
			// first find the beginning of the previous line
			int prevStart = p - 1; // index of newline
			// System.out.println("prevStart " + prevStart + " char |" +
			// text.charAt(prevStart) + "|");
			assert (text.charAt(prevStart) == '\n');
			while (prevStart - 1 >= 0 && text.charAt(prevStart - 1) != '\n')
				prevStart--;
			// System.out.println("PREV LINE BEGIN " + prevStart + " in |" +
			// text + "|");
			// find the actual indentation of the previous line
			int prevIndent = 0; // search forward from prevStart for nonspace
			while (text.charAt(prevStart + prevIndent) == ' '
					|| text.charAt(prevStart + prevIndent) == '\t')
				prevIndent++;
			// System.out.println("PREV INDENT " + prevIndent +
			// " DESIRED " + desired);
			// adjust the indentation
			int delta = desired - prevIndent;
			p = pane.getSelectionStart() + delta;
			if (delta > 0) {
				indentation = "";
				while (delta > 0) {
					indentation += " ";
					delta--;
				}
				// System.out.println("INSERT " + delta +
				// " SPACES AT " + prevStart);
				pane.setSelectionStart(prevStart);
				pane.setSelectionEnd(prevStart);
				pane.replaceSelection(indentation);
			} else if (delta < 0) {
				// System.out.println("BACKSPACE " + -delta +
				// " AT " + prevStart);
				pane.setSelectionStart(prevStart);
				pane.setSelectionEnd(prevStart - delta);
				pane.replaceSelection("");
			}
			// System.out.println("MOVE CARET TO " + p);
			pane.setSelectionStart(p);
			pane.setSelectionEnd(p);
		}
        return indent;
	}

	private int salIndentAmount(int p) {
		// how much is the default indentation?
		// p is position AFTER a newline, so back up one to get
		// the index of the newline character
		// System.out.println("salIndentAmount " + p);
		TextColor.format(this, p - 1);
		// System.out.println("salIndent return " + TextColor.indentAfter);
		return TextColor.indentAfter;
	}

	// find auto-indent position:
	// first, go back and find open paren that would match a close paren
	// second search forward to find identifier
	// if identifier is defun, defmacro, let, let*, or prog, etc.
	// indent to paren posn + 2
	// else indent to the first thing after the identifier
	int autoIndentAmount(String text, int pos) {
		int openParenLoc = findOpenParen(text, pos);
		// System.out.println("autoIndent: openParenLoc = " + openParenLoc);
		if (openParenLoc == -1)
			return 0;
		String ident = getIdentifier(text, openParenLoc + 1);
		if (ident.equals("defun ") || ident.equals("defmacro ")
				|| ident.equals("let ") || ident.equals("let* ")
				|| ident.equals("dotimes ") || ident.equals("dolist ")
				|| ident.equals("simrep ") || ident.equals("seqrep ")
				|| ident.equals("prog ") || ident.equals("prog* ")
				|| ident.equals("progv ")) {
			pos = openParenLoc + 2;
		} else {
			pos = openParenLoc + ident.length();
			System.out.println("auto-indent, pos " + pos + ", ident " + ident
					+ ", length " + ident.length());
			while (pos < text.length()
					&& Character.isWhitespace(text.charAt(pos))) {
				if (text.charAt(pos) == '\n') {
					// if the end of the line looks like "(foo \n" then the tab
					// position
					// will be indented two from the open paren (ignore the
					// identifier):
					pos = openParenLoc + 2;
					break;
				}
				pos++;
			}
			// System.out.println("pos " + pos);
		}
		return findColumnOf(text, pos);
	}

	public static boolean inComment(String text, int pos)
	// search back to newline for ";" indicating comment
	// assumes text[pos] is not escaped or in string
	{
		boolean inString = false;
		while (pos > 0) {
			char c = text.charAt(pos);
			if (c == ';')
				return true;
			if (c == '\n')
				return false;
			pos = backup(text, pos, false);
		}
		return false;
	}

	static String SalIdChars = "{}(),[]\n #\"";

	public static String getSalIdentifier(String docText, int pos) {
		// System.out.println("|" + docText + "| " + pos);
		int start = pos;
		if (pos < 0)
			return ""; // special case: no place to search from
		// allow back up over single open paren
		if (docText.charAt(pos) == '(')
			start = start - 1;
		while (start >= 0 && SalIdChars.indexOf(docText.charAt(start)) == -1) {
			start--;
		}
		// protect from bogus arguments
		if (start < -1 || pos >= docText.length())
			return "";
		// if id is terminated by open paren, substitute blank so that
		// when we search lisp-syntax wordlist we get a more precise match.
		// E.g. "osc(" becomes "osc " which will not match "osc-enable ..."
		if (docText.charAt(pos) == '(')
			return docText.substring(start + 1, pos) + " ";
		else
			return docText.substring(start + 1, pos + 1);

	}

	public static int findOpenParen(String docText, int pos) {
		int findx = -1;
		try {
			boolean inString = false; // need to get it from text color
			findx = backup(docText, pos, inString);
			if (findx == pos - 1 && findx > 0 && docText.charAt(findx) == '\\') {
				// escaped paren
				return pos;
			} else if (inComment(docText, findx)) { // in comment
				return pos;
			}
			// at this point we know there is a closed paren.
			// go back until you find the matching open paren.
			int closed = 1;
			while (findx >= 0 && closed > 0) {
				char c = docText.charAt(findx);
				if (c == '(' || c == ')') {
					if (!inComment(docText, findx)) {
						if (c == '(')
							closed--;
						else if (c == ')')
							closed++;
					}
				}
				if (closed > 0) // not done, back up
					findx = backup(docText, findx, false);
			}
		} catch (Exception e) {
			// System.out.println("findOpenParen " + e);
		}
		// System.out.println("findOpenParen returns " + findx);
		return findx;
	}

	private static int backup(String text, int pos, boolean inString)
	// find an index in text before pos by skipping over strings
	// and escaped characters of the form #\A, but do not consider
	// comment lines. If findx is zero, return result is -1.
	{
		int findx = pos - 1;
		while (true) {
			if (findx < 0) {
				return findx;
			}
			char c = text.charAt(findx);
			if (inString) {
				if (c == '"') {
					// could it be escaped?
					if (findx > 0) {
						char pre = text.charAt(findx - 1);
						if (pre == '"')
							findx--; // escaped as ""
						else if (pre == '\\')
							findx--; // escaped as \"
						else
							inString = false;
					} else
						inString = false;
				} // else keep searching for string closing
			} else { // not inString
				// test for escaped character
				if (findx > 0) {
					char pre = text.charAt(findx - 1);
					if (pre == '\\')
						findx--; // escaped
					else
						inString = (c == '"');
				} // else if c == '"' then ...
					// internal error: text begins with quote, but
					// inString is false. Just ignore it because it
					// may be that the coloring hasn't run yet
					// and all will become well.
			}
			if (!inString || findx <= 0) {
				return findx;
			}
			findx--;
		}
	}

	void synchronousUpdate() {
		// System.out.println("synchronousUpdate called");
		final CodePane codePane = this;
		final JViewport v = getViewport();
		final Point pt = v.getViewPosition();
		final Dimension e = v.getExtentSize();
		EventQueue.invokeLater(new Runnable() {
			public void run() {
				// System.out.println("calling TextColor");
				codePane.evenParens = TextColor.format(codePane, 0);
				// System.out.println("returned from TextColor");
				codePane.updateFontSize(fontSize);
			}
		});
	}

	void blinkParen(int pos) {
		try {
			String docText = doc.getText(0, pos);
			int openParenLoc = findOpenParen(docText, pos);
			if (openParenLoc >= 0 && openParenLoc < pos) {
				blink(openParenLoc);
			}
		} catch (Exception e) {
			System.out.println(e);
		}
	}

	// the blink interface: call blink(loc) to make a character blink
	void blink(int loc) {
		timer.start();
		blinkOn = true;
		blinkLoc = loc;
	}

	public void updateFontSize(int size) {
		final MutableAttributeSet attributeSet = new SimpleAttributeSet();
		StyleConstants.setFontSize(attributeSet, size);
		doc.setCharacterAttributes(0, 1000000, attributeSet, false);
	}

	public void setFontSize(int size) {
		fontSize = size;
		updateFontSize(size);
	}

}
