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 java.io.ByteArrayOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.IOException;

// 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;
    // to undo we could not get serialization of defaultstyleddocument to
    // work (hoping to use that to clone doc), so instead we save off the
    // text and cursor location. To restore, we have to recolor the doc.
    public CodeSnapshot[] undo_versions;
    public int undo_versions_index;
    // undo_versions_index is the current version in doc.
    // to undo, (when undo_versions_index > 0) undo_versions_index--
    // and use undo_versions.elementAt(undo_versions_index).
    // to redo, if undo_versions_index < undo_versions.size(), 
    // undo_versions_index++ and undo_versions.elementAt(undo_versions_index)
    // After an undoable event, if undo_versions_index == undo_versions.size(),
    // do undo_versions.addElement(content). Otherwise, 
    // undo_versions.set(undo_versions_index, content).
    // Then undo_versions_index++ and set all higher indexes to null.
    // public boolean alphanum_typed;  // set to true whenever alpha-numeric is
    // typed. This is reset by insertUpdate and used to avoid saving
    // an undo buffer for every key typed.  Also set true for delete
    // key: we do not make individual deletes undoable. (This will
    // have the not-so-good result that 10 deletes followed by some
    // alpha characters followed by undo will restore the 10 deleted
    // characters rather than just undo the run of alpha characters.
    // Also, Undo, type an alpha, Redo will go to the state prior to
    // Undo, ignoring that the user typed an alpha.)

    // Deciding when to save an Undo state: Here is my initial analysis.
    // Then, I decided that Undo should undo a string of characters followed
    // by white space rather than Undo white space characters one-by-one, so
    // we introduced another input_state for white space and rules are a
    // little more complicated. Then we introduced another state for Paste.
    // So here is a simplified version without space-glomming:
    // 
    // PS == previous_snap, a CodeSnapshot of the code state just before
    // a new character or command is typed
    // all+ = text in the code buffer + the insertion of the typed key
    //     from state 0: (undo, PS set to what was restored from undo)
    //         alphanums: state->1, make PS undoable, save all+ to PS
    //         backspace: state->2; make PS Undoable, save all to PS
    //         other: state->3; make PS Undoable, save all+ to PS
    //     from state 1: (alphanum)
    //         alphanums: keeps us in state 1, save all+ to PS
    //         backspace: state->2; make PS Undoable, save all to PS
    //         other: state->3; make PS Undoable, save all+ to PS
    //     from state 2: (backspace)
    //         alphanums: state->1: make PS undoable, save all+ to PS
    //         backspace: keep state 2; save all to PS
    //         other: state->3: make PS undoable, save all+ to PS
    //     from state 3: (other characters)
    //         alphanums: state->1; make ps Undoable, save all+ to PS
    //         backspace: state->2; make PS Undoable, save all to PS
    //         other: keep state 3; save all+ to PS
    // In simpler terms, when state changes, make PS Undoable; then
    // for backspace, save all to PS; for others save all+ to PS.
    // 
    public CodeSnapshot previous_snap;
    // public boolean last_undo_redo_was_undo;
    // 0 = undo, 1 = text, 2 = backspace, 3 = other, 4 = whitespace,
    // 5 = paste
    public int input_state;
    public boolean format_should_snapshot;

    public int SHORTCUT_MASK = 
            System.getProperty("os.name").toLowerCase().contains("mac")
            ? InputEvent.META_DOWN_MASK  // Command key on Mac
            : InputEvent.CTRL_DOWN_MASK; // Ctrl key elsewhere
    
    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;
    int indentedPosAfterNewline;
    // boolean doUndo;
    int synchronous_update_nesting;  // avoid recursive formatting
    boolean needs_to_be_formatted;  // helps to format just once per change
    boolean internal_update;  // used to distinguish IDE modifications from
    boolean is_win32;
    // user modifications such as Paste that need to be undoable.
    Font mainFont;

    final int TIMER_DELAY = 1000; // ms

    public class CodeSnapshot {
        public String content;
        public int cursor_posn;

        public CodeSnapshot(String text, int posn) {
            if (is_win32) {
                // for some reason, when you get text from doc, 
                // it has CR instead of LF, so fix it.
                text = text.replace('\r', '\n');
            }
            reset(text, posn);
        }

        public void reset(String text, int posn) {
            content = text;
            cursor_posn = posn;
        }

        public void show() {
            /*
            System.out.print("CodeSnapshot:\n|");
            for (int i = 0; i < content.length(); i++) {
                if (i == cursor_posn) {
                    System.out.print("<cursor>");
                }
                char c = content.charAt(i);
                if ((c == 10) || (c >= 32 && c <= 126)) {
                    System.out.print(c);
                } else {
                    System.out.print("<" + (int) c + ">");
                }
            }
            if (cursor_posn == content.length()) {
                System.out.print("<cursor>");
            } else if (cursor_posn > content.length()) {
                System.out.print("[CURSOR AT " + cursor_posn + "]");
            }
            System.out.println("| (len " + content.length() + ")");
            */
        }

        public void toPane(JTextPane pane, DefaultStyledDocument doc) {
            try {
                int expected = content.length();
                doc.replace(0, doc.getLength(), content,
                            doc.getCharacterElement(0).getAttributes());
                // Windows implementation inserts extra LF at end (sometimes?):
                // (Maybe the problem is elsewhere, but this doesn't hurt.)
                if (is_win32 && doc.getLength() > expected) {
                    doc.remove(expected, doc.getLength() - expected);
                }
            } catch (BadLocationException e) {
                System.out.println(e);
                return;
            }
            System.out.println("CodeSnapshot.toPane content is:");
            show();
            System.out.println("CodeSnapshot.toPane after set doc:");
            doc_print();
            System.out.println("CodeSnapshot.toPane setting cursor posn " +
                               cursor_posn);
            pane.setCaretPosition(cursor_posn);
        }
    }


/*
    public static DefaultStyledDocument cloneStyledDoc(
            DefaultStyledDocument source) {
        try {
            DefaultStyledDocument retDoc = new DefaultStyledDocument();
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(source); // write object to byte stream
            ByteArrayInputStream bis = new ByteArrayInputStream(
                                              bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            //read object from stream:
            retDoc = (DefaultStyledDocument) ois.readObject();
            ois.close();
          return retDoc;
        } 
        catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
*/

    public CodePane(Dimension preferredSize, MainFrame mf, 
                    JLabel sb, int fontSz) {
        super();
        blinkLoc = 0;
        blinkOn = false; // initialize
        fontSize = fontSz;
        mainFrame = mf;
        statusBar = sb;
        indentedPosAfterNewline = -1;
        // doUndo = true; // normally actions are undoable, see hack below
        isSal = false;
        doc = new DefaultStyledDocument();
        pane = new JTextPane(doc);
        undo_versions = new CodeSnapshot[200];
        undo_versions_index = 0;
        internal_update = true;
        is_win32 = System.getProperty("os.name").toLowerCase().
                   contains("win");
        previous_snap = new CodeSnapshot("", 0);
        insert_undo_point(previous_snap);  // initially can undo to empty
        format_should_snapshot = false;

        // last_undo_redo_was_undo = true;  // doesn't really matter
        input_state = 1;  // initial state is "text"

        // alphanum_typed = false;
        mainFont = new Font("sdfi", Font.PLAIN, 11);
        if (mainFont == null) {
            mainFont = new Font("Lucida Console", Font.PLAIN, 11);
        }
        pane.setFont(mainFont);
        synchronous_update_nesting = 0;
        needs_to_be_formatted = false;
        
        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") &&
        //            doUndo) {
        //            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) {
                undo();
            }
        });


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

        // Bind the redo action to ctl-Y
        // pane.getInputMap().put(KeyStroke.getKeyStroke("control Y"), "Redo");
        // Bind the undo action to CMD-Y for Mac
        // pane.getInputMap().put(KeyStroke.getKeyStroke(
        //        java.awt.event.KeyEvent.VK_Y,
        //        Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "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 insert_undo_point(CodeSnapshot code_snap) {
        // limit number of undo steps to length of undo_versions:
        if (undo_versions_index >= undo_versions.length) {
            System.out.println("************* limit undo versions *****");
            for (int i = 1; i < undo_versions.length; i++) {
                undo_versions[i - 1] = undo_versions[i];
            }
            // clear the last element which was moved
            undo_versions[undo_versions.length - 1] = null;
            System.out.println("############# decrement undo_versions_index " +
                               "in insert_undo_point (moved)");    
            undo_versions_index--;  // it moved!
        }
        System.out.println("insert_undo_point: installing snapshot at index " +
                           undo_versions_index + " and setting index to " +
                           (undo_versions_index + 1) + " cursor " +
                           code_snap.cursor_posn);
        undo_versions[undo_versions_index] = code_snap;
        System.out.println("$$$$$$$$$$$$$$ increment undo_versions_index " +
                           "in insert_undo_point");
        undo_versions_index++;
        for (int i = undo_versions_index; i < undo_versions.length; i++) {
            if (undo_versions[i] != null) {
                System.out.println("    clearing index " + i);
            }
            undo_versions[i] = null;  // clear because can no longer redo
        }
    }

    public void redo() {
        // When we reverse direction of undo/redo, we need to skip
        // over a version and go forward one more:
        if (undo_versions_index + 1 < undo_versions.length &&
            undo_versions[undo_versions_index + 1] != null) {
            undo_versions_index++;
            CodeSnapshot code_snap = undo_versions[undo_versions_index];
            code_snap.toPane(pane, doc);
            System.out.println("redo: restoring snapshot from index " +
                               undo_versions_index + " and leaving index at " +
                               undo_versions_index);
            /*
            System.out.println("$$$$$$$$$$$$$$ increment undo_versions_index " +
                               "in redo (after restore)");
            undo_versions_index++;
            */
            // pane.revalidate();  // not sure this is necessary
        } else {
            System.out.println("---redo(): nothing left to redo");
        }
        // last_undo_redo_was_undo = false;
        input_state = 0;
    }

    public void undo() {
        // implement the undo command
        if (input_state != 0) {
            // if we were typing, save what we typed so we can redo it
            insert_undo_point(previous_snap);
            System.out.println("############# decrement undo_versions_index " +
                               "in undo (we were typing)");
            undo_versions_index--;
        } /* else if (!last_undo_redo_was_undo && undo_versions_index > 0) {
            // When we reverse direction of undo/redo, we need to skip
            // over a version and go back one more:
            System.out.println("############# decrement undo_versions_index " +
                               "in undo (!last_undo_redo_was_undo)");
            undo_versions_index--;
            } */
        if (undo_versions_index > 0) {
            System.out.println("############# decrement undo_versions_index " +
                               "in undo (undo)");
            undo_versions_index--;
            CodeSnapshot code_snap = undo_versions[undo_versions_index];
            code_snap.show();
            code_snap.toPane(pane, doc);
            // last_undo_redo_was_undo = true;
        } else {
            System.out.println("---undo(): nothing left to undo");
        }
        System.out.println("undo: after undo, index is " + undo_versions_index);
        input_state = 0;  // undo
    }

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

    public void removeUpdate(DocumentEvent e) {
        synchronousUpdate();
        // if (!alphanum_typed) {
        //     insert_undo_point();
        // }
        // alphanum_typed = false;
    }

    public void insertUpdate(DocumentEvent e) {
        if (!internal_update) {
            insert_undo_point(previous_snap);
            snapshot();
            input_state = 5;  // paste
        }
        synchronousUpdate();
    }

    public void caretUpdate(CaretEvent e) {
        if (e.getDot() == e.getMark()) {
            updateCaretLoc();
        }
    }

    public void adjustmentValueChanged(AdjustmentEvent evt) {
        System.out.println("adjustmentValueChanged calls synchronousUpdate");
        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) {
        if ((ke.getModifiersEx() & SHORTCUT_MASK) != 0) {
            System.out.println("keyReleased CMD- or CTRL-" + 
                               Character.toString((char) ke.getKeyCode()));
            // revert to normal ignoring document inserts/deletes
            internal_update = true;
        }
        return;
    }

    public void keyPressed(KeyEvent ke) {
        if ((ke.getModifiersEx() & SHORTCUT_MASK) != 0) {
            int kc = ke.getKeyCode();
            System.out.println("keyPressed CMD- or CTRL-" + 
                               Character.toString((char) kc));
            // whatever happens is not internally caused, but exclude Z:
            if (kc == KeyEvent.VK_C || kc == KeyEvent.VK_V || 
                kc == KeyEvent.VK_X) {
                internal_update = false; // cause change to be snapshotted
            } // otherwise, undo/redo can manage themselves
        }
        return;
    }

    public String fix_text(String alltext) {
        // It seems like sometimes getText() returns extra; fix it:
        if (is_win32 && alltext.length() > doc.getLength()) {
            // maybe it has CRLF instead of LF:
            int len = alltext.length();
            alltext = alltext.replace("\r\n", "\n");
            if (len != alltext.length()) {
                System.out.println("---- alltext had CRLF!");
            }
        }
        if (is_win32 && alltext.length() > doc.getLength()) {
            // if that doesn't fix it, try again:
            System.out.println("---- alltext was too long!");
            alltext = alltext.substring(0, doc.getLength());
        }
        return alltext;
    }


    public void keyTyped(KeyEvent ke) {
        char ch = ke.getKeyChar();
        int next_state;
        if ((ke.getModifiersEx() & SHORTCUT_MASK) != 0
            && ch >= 0 && ch <= 26) {
            // ASCII "sub" character, which Windows injects when you type
            // CTRL-Z, and Java propagates -- tell me again why Java adds
            // so many abstractions if it can't even come up with an
            // abstraction for UNDO, or for that matter even typing CTRL-Z?
            // I figure this has caused portability problems and a few 
            // hours of effort about 1000 times, so this one Java design
            // flaw cost $300,000? But I digress..
            // AIIEEE! Windows injects other characters for CTRL-A to 
            // CTRL-Z. Conditional is designed to catch more foolishness.
            System.out.println("Ignoring keyTyped " + (int) ch);
            return;
        }
        System.out.println("\n\nCodePane keyTyped " +
                           ((ch >= 32 && ch <= 126) 
                            ? "|" + Character.toString(ch) + "|" 
                            : (int) ch));

        String alltext = fix_text(pane.getText());

        if (Character.isLetterOrDigit(ch)) {
            next_state = 1;  // alphanum
        } else if (ch == 8) {
            next_state = 2;   // delete
        } else if (Character.isWhitespace(ch)) {
            next_state = 4;   // whitespace
        } else {
            next_state = 3;   // other
        }

        if (input_state != next_state) {
            if (input_state == 0) {  // we restored to undo_versions_index
                // but now we have input, so we want to keep the current
                // CodeSnapshot so that we can undo this current input
                System.out.println("$$$$$$$$$$$$$$ increment undo_versions_" +
                                   "index in keyTyped (state change from 0)");
                undo_versions_index++;
            } else {  // we typed something previously, so that becomes
                      // the snapshot we can undo to
                if ((input_state == 1 || input_state == 3) && next_state == 4) {
                    ; // white-space is glommed onto any text input so we
                    // undo past space to a string of non-space characters
                } else {
                    insert_undo_point(previous_snap);
                }
            }
        }

        // 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:
        int pos = pane.getCaretPosition();
        System.out.println("keyTyped caret position: " + pos);
        alltext = null;

        // backspace can take out indentation immediately after newline that
        // generates indentation, but not otherwise, so clear
        // indentedPosAfterNewline as soon as something is typed (other than
        // backspace):
        if (ch != '\b') {
            indentedPosAfterNewline = -1;
        }
        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);
                }
                // TODO: probably clear Undo state after command given
            } else { // not the command entry window
                if (insertIndentation(pos) > 1) {
                    indentedPosAfterNewline = pane.getCaretPosition();
                    // the indentation will be removed at once
                } // otherwise a backspace will either delete one space
                  // or one newline so we do not want 
                  // indentedPosAfterNewline 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' && 
                   indentedPosAfterNewline - 1 == pane.getCaretPosition()) {
            // the user's backspace put us one character back from where we
            // indented multiple spaces -- delete them all
            indentedPosAfterNewline = -1; // 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
            StringBuilder sb = new StringBuilder(pane.getText());
            sb.insert(pos, ch);
            alltext = sb.toString();
                      
            if (ch == ')') {
                // get caret location
                blinkParen(pos);
            } else if (ch == '(') {
                if (MainFrame.prefParenAutoInsert) {
                    pane.replaceSelection(")");
                    pane.setCaretPosition(pos);
                }
            } else {  // typing ordinary character replaces selection if any
                pane.replaceSelection("");
            }
            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--;
                System.out.println("normal backspace, pos " + 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 + 1;
                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) == ' ');
            }
            // put up words list
            WordList.printList(identifier, pane, identLoc, pos + 1, forceExact,
                    isSal);
        }

        /*
        // undo processing (putting previous_snap in Undo list handled earlier)
        pos = pane.getCaretPosition();
        if (alltext == null) {
            alltext = fix_text(pane.getText());
        } else {  // we are inserting a character, so pos should come after it
            alltext = fix_text(alltext);
            pos++;
            System.out.println("After keyTyped before snap, ch " + (int) ch +
                               " pos " + pos + " is_win32 " + is_win32);
        }
        // if this is Windows and we insert a LF, somehow a CR gets
        // inserted too, so we need to increase by another character:
        */
        /*
        if (ch == 10 && is_win32) {
            pos++;
            System.out.println("After keyTyped 10 before snap, pos " + pos);
        }
        */
        /* previous_snap = new CodeSnapshot(alltext, pos);
        System.out.println("after keyTyped, pos is " + pos + " previous_snap:");
        previous_snap.show();
        */
        format_should_snapshot = true;
        input_state = next_state;
        
        forceReformatHack();
    }

    // apparently, a bug was introduced into Swing where line-wrap markers
    // are not updated when text is inserted. To work around this problem,
    // this method clears out the whole document, puts it back, and runs the
    // syntax coloring again. That should do it. There's probably a faster
    // simpler cleaner way, e.g. these edits have to be excluded from undo 
    // using another hack.
    public void forceReformatHack() {
        try {
            // doUndo = false;  // disable making these events undoable
            int pos = pane.getCaretPosition();  // save it to restore below
            int docLen = doc.getLength();
            if (docLen > 0) {
                String docText = doc.getText(0, docLen - 1);
                pane.setSelectionStart(0);
                pane.setSelectionEnd(docLen - 1);
                pane.replaceSelection(docText);
                pane.setCaretPosition(pos);
            }
        } catch (Exception ex) {
            System.out.println("exception getting docText" + ex);
        }
    }


    // 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() {
        if (synchronous_update_nesting > 0 || needs_to_be_formatted) {
            return;  // we're already planning to do a format
        }
        needs_to_be_formatted = true;
        synchronous_update_nesting++;
        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() {
                codePane.updateFontSize(fontSize);
                // System.out.println("calling TextColor");
                codePane.evenParens = TextColor.format(codePane, 0);
                // System.out.println("returned from TextColor");
                needs_to_be_formatted = false;  // enable next request
                if (format_should_snapshot) {
                    format_should_snapshot = false;
                    snapshot();
                    System.out.println("in format because format_should_" +
                            "snapshot pos is " + previous_snap.cursor_posn +
                            " previous_snap:");
                    previous_snap.show();
                }
            }
        });
        synchronous_update_nesting--;
    }

    void snapshot() {
        String alltext = fix_text(pane.getText());
        int pos = pane.getCaretPosition();
        previous_snap = new CodeSnapshot(alltext, pos);
    }

    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 doc_print() {
        /*
        try {
            int len = doc.getLength();
            for (int i = 0; i < len; i++) {
                char c = doc.getText(i, 1).charAt(0);
                System.out.print("" + i + ": ");
                if (c >= 32 && c <= 126) {
                    System.out.print("'" + c + "' ");
                }
                System.out.println("(" + (int) c + ")");
            }
        } catch (BadLocationException e) {
            e.printStackTrace();
        }
        */
    }


    public void updateFontSize(int size) {
        // final MutableAttributeSet attributeSet = new SimpleAttributeSet();
        if (doc.getLength() > 0) {
            System.out.println(
                    "calling doc.setCharacterAttribute (font size) " +
                    size + " with doc length " + doc.getLength());

            doc_print();
            
            TextColor.init(size);
            StyleContext styleContext = StyleContext.getDefaultStyleContext();
            AttributeSet attrs = styleContext.addAttribute(
                    SimpleAttributeSet.EMPTY, StyleConstants.FontSize, size);
            doc.setCharacterAttributes(0, doc.getLength() + 1, attrs, false);
        }
    }

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

}
