//
// InputPane.java                           Copyright A C Norman, 2000
//
//
//


import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.datatransfer.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import java.util.*;


class InputPane extends JTextPane
{
// This extends a JTextPane since that lets me hook onto all the
// actions and general facilities that that provides me with. But what
// I first do here is to get myself hooks onto all those existing actions
// and then install a new keymap that directs most things to my own set of
// actions. During development these will just invoke the one that I had
// inherited, often printing a message so I can verify what is going on.
// But eventually they implement the behaviour that I really want, much of
// which is described in the file CWin.java

StyledDocument doc;

// A InputPane can be in one of four states.

int state = OUTPUTMODE;

static final int INPUTMODE   = 0;
static final int OUTPUTMODE  = 1;
static final int PAGEWAIT    = 2;
static final int INTERRUPTED = 3;

static final int redColor    = 0x00ff0040;   // slight purple tinge
static final int yellowColor = 0x00ffffc0;   // rather pale yellow
static final int blueColor   = 0x000000c0;   // darkish blue
static final int greenColor  = 0x00008040;   // darkish green
static final int blackColor  = 0x00000040;   // very dark blue

SimpleAttributeSet redText;
SimpleAttributeSet yellowText;
SimpleAttributeSet blueText;
SimpleAttributeSet greenText;
SimpleAttributeSet blackText;

String monospacedFontName = "MonoSpaced";
String fontName = "Serif";
int fontSize = 16;
int fontWeight = Font.PLAIN;
Font fixedFont, font, smallFont, tinyFont;
Font italicFont, smallItalicFont, tinyItalicFont;
FontMetrics metrics;

DefaultCaret caret;

public boolean getScrollableTracksViewportWidth()
{
//  System.out.println("check width option");
    return true; // (doc.getLength() == 0);
}

class ToScreen implements Runnable
{
    String b;

    ToScreen(String s)
    {   b = s;
    }
    
    public void run()
    {
        try
	{   doc.insertString(doc.getLength(), b, null);
	    caret.setDot(doc.getLength());
	}
	catch (BadLocationException e)
	{}
    }
}

// toScreen is called from other threads - I must protect Swing against 
// that!

void toScreen(String b)
{
    SwingUtilities.invokeLater(new ToScreen(b));
}

int afterPrompt = 0;


void showPrompt()
{
    try
    {   doc.insertString(doc.getLength(), 
            (Fns.prompt==null ? "> " : Fns.prompt),
            redText);
        afterPrompt = doc.getLength();
        caret.setDot(afterPrompt);
	scrollRectToVisible(caret);
    }
    catch (BadLocationException e)
    {}
}

Action [] pendingActions = new Action[10];
ActionEvent [] pendingEvents = new ActionEvent[10];
int pendingIn = 0, pendingOut = 0;

void deferAction(Action a, ActionEvent e)
{
    int l = pendingActions.length;
    pendingActions[pendingIn] = a;
    pendingEvents[pendingIn++] = e;
    if (pendingIn == l) pendingIn = 0;
    if (pendingIn == pendingOut) // buffer now full
    {   Action [] w = new Action[2*l];
        ActionEvent [] w1 = new ActionEvent[2*l];
        pendingIn = 0;
        for (int i=pendingOut; i<l; i++) 
        {   w[pendingIn] = pendingActions[pendingOut+i];
            w1[pendingIn++] = pendingEvents[pendingOut+i];
        }
        for (int i=0; i<pendingOut; i++)
        {   w[pendingIn] = pendingActions[i];
            w1[pendingIn++] = pendingEvents[i];
        }
        pendingOut = 0;
        pendingActions = w;
        pendingEvents = w1;
    }
}

void performPendedActions()
{
    int l = pendingActions.length;
    while (state == INPUTMODE && pendingIn != pendingOut)
    {   Action a = pendingActions[pendingOut];
        ActionEvent e = pendingEvents[pendingOut++];
        if (pendingOut == l) pendingOut = 0;
        a.actionPerformed(e);
    }
}

String inputData = null;

// getInputLine will be performed on a task thread, not on the
// event dispatch thread. Therefor I have to do funny things to
// make it valid for working with Swing.


class RequestInput implements Runnable
{
    public void run()
    {
         showPrompt();
	 state = INPUTMODE;
	 performPendedActions();
    }
}

Runnable request = new RequestInput();

synchronized String getInputLine()
{
    SwingUtilities.invokeLater(request); // wake up the event dispatch thread!
// I  use a shared variable "inputData" to transfer data back. But if I
// make all methods that touch it synchonized I should be OK even there.
    while (inputData == null)
    {
// Suppose I am unlucky and the pended actions do not include a newline
// but one is typed while I am just about HERE. Then a notify() is
// done but I miss it! Well in fact by giving a time-out here I am pretty
// much OK. In around 0.2 of a second I will come round and detect that
// inputData has become non-null and so I will exit the loop. Whew!
// The 0.2 sec choice is to keep the overhead of busy-waiting under
// some degree of control while trying to appear responsive in even this
// odd case. My expectation is that USUALLY the newline will not arrive
// until I am safely within the wait() so there will not be any undue
// delay.
        try
        {   wait(200);
        }
        catch (InterruptedException e)
        {}
    }
    String d = inputData; // NB the newline is implicit
    inputData = null;
    return d;
}

synchronized void setInputData(String s)
{
    inputData = s;
    state = OUTPUTMODE;
    notify();
}

HashMap actionHash = new HashMap();

Action getAction(String name)
{
    return (Action)actionHash.get(name);
}

Action oldcopy,               copy;               
Action oldcut,                cut;                
Action olddefaultKeyTyped,    defaultKeyTyped;    
Action olddeleteNextChar,     deleteNextChar;     
Action olddeletePrevChar,     deletePrevChar;     
Action oldinsertBreak,        insertBreak;        
Action oldinsertContent,      insertContent;      
Action oldinsertTab,          insertTab;          
Action oldpaste,              paste;              
Action                        historyUp;
Action                        historyDown;
Action                        insertPasted;

Vector history = new Vector(10, 10);
int historyIndex;

InputPane myself;

InputPane()
{
    myself = this;
    doc = getStyledDocument();

    setPreferredSize(new Dimension(800, 200));
    setEditable(true);

    guessFont();
    setupFonts(fontSize, fontWeight);

    caret = (DefaultCaret)getCaret();
    requestFocus();

    setBackground(new Color(yellowColor)); // a pale yellow
    setForeground(new Color(blackColor));  // very dark blue!

// While updating the contents of the document I will often want to
// set attributes, specifically ones that are to do with the colour of
// the text inserted. Here are some attribute values ready to help me.
    redText   = new SimpleAttributeSet();
    yellowText= new SimpleAttributeSet();
    blueText  = new SimpleAttributeSet();
    greenText = new SimpleAttributeSet();
    blackText = new SimpleAttributeSet();
    StyleConstants.setForeground(redText, new Color(redColor));
    StyleConstants.setForeground(yellowText, new Color(yellowColor));
    StyleConstants.setForeground(blueText, new Color(blueColor));
    StyleConstants.setForeground(greenText, new Color(greenColor));
    StyleConstants.setForeground(blackText, new Color(blackColor));


    Action [] a = getActions();
    for (int i=0; i<a.length; i++)
        actionHash.put(a[i].getValue(Action.NAME), a[i]);

    oldcopy            = getAction(StyledEditorKit.copyAction);
    oldcut             = getAction(StyledEditorKit.cutAction);
    olddefaultKeyTyped = getAction(StyledEditorKit.defaultKeyTypedAction);
    olddeleteNextChar  = getAction(StyledEditorKit.deleteNextCharAction);
    olddeletePrevChar  = getAction(StyledEditorKit.deletePrevCharAction);
    oldinsertBreak     = getAction(StyledEditorKit.insertBreakAction);
    oldinsertContent   = getAction(StyledEditorKit.insertContentAction);
    oldinsertTab       = getAction(StyledEditorKit.insertTabAction);
    oldpaste           = getAction(StyledEditorKit.pasteAction);

    copy            = new CopyAction();
    cut             = new CutAction();
    defaultKeyTyped = new DefaultKeyTypedAction();
    deleteNextChar  = new DeleteNextCharAction();
    deletePrevChar  = new DeletePrevCharAction();
    insertBreak     = new InsertBreakAction();
    insertContent   = new InsertContentAction();
    insertTab       = new InsertTabAction();
    paste           = new PasteAction();
    historyUp       = new HistoryUpAction();
    historyDown     = new HistoryDownAction();
    insertPasted    = new InsertPastedAction();

    Keymap map = getKeymap();

    map.setDefaultAction(new DefaultKeyTypedAction());
    map.addActionForKeyStroke(        // ^X => CUT
        KeyStroke.getKeyStroke(KeyEvent.VK_X, Event.CTRL_MASK),
        cut);
    map.addActionForKeyStroke(        // ^V => PASTE
        KeyStroke.getKeyStroke(KeyEvent.VK_V, Event.CTRL_MASK),
        paste);
    map.addActionForKeyStroke(        // ^C => COPY
        KeyStroke.getKeyStroke(KeyEvent.VK_C, Event.CTRL_MASK),
        copy);
    map.addActionForKeyStroke(        // backspace => deletePrev
        KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0),
        deletePrevChar);
    map.addActionForKeyStroke(        // delete => deleteNext
        KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0),
        deleteNextChar);
    map.addActionForKeyStroke(        // enter => accept the line
        KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
        insertBreak);
    map.addActionForKeyStroke(        // tab => tab
        KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0),
        insertTab);
// The default actions for the up and down keys are
//     up/down          move up and down one line
//     shift+up/down    ditto but extending a selection
//     ctrl+up/down     up & down one paragraph
//     ctrl+shift       select up and sown by the paragraph
// I modify this so that ctrl+up/down gives a behaviour rather like
// that of "doskey" or the command history scheme present in some
// Unix shells. It replaces the whole of the current input line
// with one of the previous input lines (which I therefore need to
// store away)
    map.addActionForKeyStroke(        // ctrl-UP
        KeyStroke.getKeyStroke(KeyEvent.VK_UP, Event.CTRL_MASK),
        historyUp);
    map.addActionForKeyStroke(        // ctrl-DOWN
        KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, Event.CTRL_MASK),
        historyDown);
    setKeymap(map);

    requestFocus();
    replaceSelection("\n" +
//                   getPreferredScrollableViewportSize() + "\n" +
                     getBounds() + "\n" +
                     getWidth() + "\n"
                     );


}

void guessFont()
{
    fontSize = 16;
    fontWeight = Font.PLAIN;
}

void setupFonts(int size, int weight)
{
    fontSize = size;
    fontWeight = weight;
// I will prepare seven fonts. The first is a fixed pitch one used for
// simple text output. The remaining seven (and I may decide later on that
// I need yet more) are Roman & Italic styles in three sizes: the intent is
// that they are for body, subscript and sub-subscripts.
    fixedFont = new Font(monospacedFontName, fontWeight, fontSize);
    setFont(fixedFont);
    metrics = getFontMetrics(fixedFont);
// Now the others... I am somewhat uncertain about the scaling
// rules that I should apply here, but I am confident that if the
// main font is really tiny already I should be cautious about
// reducing too abruptly. Tune this for the best visual effect!
    font      = new Font(fontName, fontWeight, fontSize);
    italicFont = new Font(fontName, fontWeight+Font.ITALIC, fontSize);
    int smallSize = (fontSize <= 10) ? fontSize-1 :
                    (fontSize <= 14) ? fontSize-2 :
                    ((7*fontSize+9)/10);
    smallFont = new Font(fontName, fontWeight, smallSize);
    smallItalicFont = new Font(fontName, fontWeight+Font.ITALIC, smallSize);
    int tinySize = (smallSize <= 10) ? smallSize-1 :
                    (smallSize <= 14) ? smallSize-2 :
                    ((7*smallSize+9)/10);
    tinyFont  = new Font(fontName, fontWeight, tinySize);
    tinyItalicFont  = new Font(fontName, fontWeight+Font.ITALIC, tinySize);
}



// I want to arrange that input up to and including the most
// recent prompt string is read-only. To achieve this I arrange
// to reset dot and mark to be beyond there whenever an interesting
// event is triggered. I allow them to roam so I can COPY from earlier
// material.

int adjustSelection()
{
    int dot = caret.getDot();
    int mark = caret.getMark();
    int x = afterPrompt;
    if (dot < afterPrompt)
    {   if (mark < afterPrompt)
        {   caret.setDot(afterPrompt);
            dot = mark = afterPrompt;
        }
        else
        {   caret.setDot(mark);
            caret.moveDot(afterPrompt);
            dot = afterPrompt;
        }
    }
    else if (mark < afterPrompt)
    {   caret.setDot(afterPrompt);
        caret.moveDot(dot);
        mark = afterPrompt;
    }
    if (mark < dot) return mark;
    else return dot;
}

String clipboardSubstitute = null;

class CopyAction extends AbstractAction
{
    CopyAction()
    {   super(StyledEditorKit.copyAction);
    }

    public void actionPerformed(ActionEvent e)
    {
// I possibly want to customise this to transfer some style information
// to the clipboard as well as plain text. Eg I probably want to do special
// things with prompt strings.  But also note that I want COPY (and so also
// CUT) to be executed instantly and not deferred waiting for input. Actually
// that means that CUT will not do much cutting in such circumstances.
//
        int mark = caret.getMark();
        int dot = caret.getDot();
        if (mark == dot) return;
        else if (mark > dot)
        {   int w = mark;
            mark = dot;
            dot = w;
        }
// now (mark < dot) defines the region of my selection
            StringBuffer s = new StringBuffer();
// This really feels pretty gross. I look at each character and discard it
// if it is the colour that I use for prompts! Doing it one character at a
// time makes COPY a slow process, but I expect it only gets used on smallish
// selections!  The reasoning behind this is that I may want to re-input
// previous segments of stuff, and if the prompts get picked up by "COPY"
// and put back by "PASTE" that will mess me up.
//
// Maybe a yet better variation will be to map things so that the
// text on the clipboard reads
//       on screen         on clipboard
//          \                 \\ 
//          {                 \{
//          }                 \}
//          prompt            {\p prompt}
// where this leaves capability for other bits of clipboard syntax
// involving {} to denote other effects I may later introduce.
//
// Anyway that is not yet done and needs more thought!
        for (int i=mark; i<dot; i++)
        {   try
            {   Element ch = doc.getCharacterElement(i);
                AttributeSet a = ch.getAttributes();
                Color col = doc.getForeground(a);
                if (new Color(redColor).equals(col)) continue;
                String chs = doc.getText(i, 1);
                s.append(chs);
            }
            catch (BadLocationException e1)
            {}
        }
        try
        {   Clipboard cb = getToolkit().getSystemClipboard();
            StringSelection ss = new StringSelection(s.toString());
            cb.setContents(ss, ss);
        }
        catch (SecurityException e1)
        {   clipboardSubstitute = s.toString();
        }
    }
}

class CutAction extends AbstractAction
{
    CutAction()
    {   super(StyledEditorKit.cutAction);
    }

    public void actionPerformed(ActionEvent e)
    {
// I want to copy it ALL to the clipboard (and I use the current
// version of that action so I can deal with style-specific stuff
// sometime when I feel keen). But then I delete just that bit
// which is within the current input line.
        copy.actionPerformed(e);
        if (state != INPUTMODE) return;
        adjustSelection();
        replaceSelection("");
    }
}

class DefaultKeyTypedAction extends AbstractAction
{
    DefaultKeyTypedAction()
    {
        super(StyledEditorKit.defaultKeyTypedAction);
    }

    public void actionPerformed(ActionEvent e)
    {
        int c = (int)e.getActionCommand().charAt(0);
// I will be rather cautious about what characters I insert into my text.
// In particular and perhaps unreasonably I will NOT allow characters with
// code 0x100 or upwards. I also forbid control characters and deletes.
        if ((c & 0x7f) < 0x20 || (c & 0x7f) == 0x7f || c >= 0x100 ||
             (e.getModifiers() & 
              (Event.CTRL_MASK|Event.ALT_MASK|Event.META_MASK)) != 0) return;
        if (state != INPUTMODE)
        {   deferAction(this, e);
            return;
        }
// NB the "dot" returned here is the lower of dot/mark and so it is the
// position to insert after I have done a replaceSelection("");
        int dot = adjustSelection();
// I replace the selection with an empty string and THEN insert my
// new character because that lets me control the attributes (in my case
// colour) of inserted text.
        replaceSelection("");
        try
        {   doc.insertString(dot, e.getActionCommand(), blueText);
            caret.setDot(dot+1);
        }
        catch (BadLocationException e1)
        {}
    }
}

int adjustForDelete(boolean left)
{
// When I hit either delete-forwards or delete-backwards I will behave
// as follows:
//     If either dot or mark line within the current input line
//     I shrink the selection to be the intersection of the current one
//     and the input line. If both dot and mark are outside the
//     current line then delete-forwards moves them to its start and
//     delete backwards moves them both to its end.
//     If there is a non-empty selection left I delete that material and
//     that is all I do (whichever direction the delete was in)
//     Finally I have a caret location within the input line and if
//     there is a character to the relevant size of it I delete that.
    int dot = caret.getDot();
    int mark = caret.getMark();
    int x = afterPrompt;
    if (dot < afterPrompt)
    {   if (mark < afterPrompt)
        {   if (left)
            {   dot = mark = doc.getLength();
                caret.setDot(dot);
            }
            else
            {   dot = mark = afterPrompt;
                caret.setDot(afterPrompt);
            }
        }
        else
        {   caret.setDot(mark);
            caret.moveDot(afterPrompt);
            dot = afterPrompt;
        }
    }
    else if (mark < afterPrompt)
    {   caret.setDot(afterPrompt);
        caret.moveDot(dot);
        mark = afterPrompt;
    }
// now the selection is fully after the prompt location
    if (mark == dot) return dot;
    replaceSelection("");
    return -1;
}


class DeleteNextCharAction extends AbstractAction
{
    DeleteNextCharAction()
    {   super(StyledEditorKit.deleteNextCharAction);
    }

    public void actionPerformed(ActionEvent e)
    {
        if (state != INPUTMODE)
        {   deferAction(this, e);
            return;
        }
        int dot = adjustForDelete(false);
        if (dot < 0) return; // done
        if (dot == doc.getLength()) return;
        caret.setDot(dot);
        caret.moveDot(dot+1);
        replaceSelection("");
    }
}

class DeletePrevCharAction extends AbstractAction
{
    DeletePrevCharAction()
    {   super(StyledEditorKit.deletePrevCharAction);
    }

    public void actionPerformed(ActionEvent e)
    {
        if (state != INPUTMODE)
        {   deferAction(this, e);
            return;
        }
        int dot = adjustForDelete(true);
        if (dot <= afterPrompt) return;
        caret.setDot(dot);
        caret.moveDot(dot-1);
        replaceSelection("");
    }
}

class InsertBreakAction extends AbstractAction
{
    InsertBreakAction()
    {   super(StyledEditorKit.insertBreakAction);
    }

    public void actionPerformed(ActionEvent e)
    {
        if (state != INPUTMODE)
        {   deferAction(this, e);
            return;
        }
        int l = doc.getLength();
        try
        {   String s = getText(afterPrompt, l - afterPrompt);
            history.add(s);  // remember it!
            historyIndex = history.size();
// now I will put a newline on the screen.
            caret.setDot(l); // so it tends to remain at the end...
            doc.insertString(l, "\n", blueText);
// I should do the setInputData LAST here since it flips my global
// state from INPUTMODE to OUTPUTMODE and also lets the client thread
// have my data and potentially start generating new output based on it.
// Actually I might worry a bit about what happens if another mouse or
// keyboard event is triggered while I am busy around here...
            setInputData(s);
	}
        catch (BadLocationException e1)
        {}
    }
}

// The next action is ONLY used as a pending action, so it can only
// ever be triggered when I am in input mode.

class InsertPastedAction extends AbstractAction
{
    InsertPastedAction()
    {
        super("insert-pasted-text");
    }

    public void actionPerformed(ActionEvent e)
    {
        int dot = adjustSelection();
        replaceSelection("");
        try
        {   String s = e.getActionCommand();
            doc.insertString(dot, s, blueText);
            caret.setDot(dot+s.length());
        }
        catch (BadLocationException e1)
        {}
        if (e.getModifiers() != 0) return;
        insertBreak.actionPerformed(e);
    }
}

class InsertContentAction extends AbstractAction
{
    InsertContentAction()
    {   super(StyledEditorKit.insertContentAction);
    }

    public void actionPerformed(ActionEvent e)
    {
        System.out.println("insertContent " +
                           safe(e.getActionCommand()) + " " + 
                           safe(e.paramString()));
//      oldinsertContent.actionPerformed(e);
    }
}

class InsertTabAction extends AbstractAction
{
    InsertTabAction()
    {   super(StyledEditorKit.insertTabAction);
    }

    public void actionPerformed(ActionEvent e)
    {
        if (state != INPUTMODE)
        {   deferAction(this, e);
            return;
        }
        int dot = adjustSelection();
        replaceSelection("");
        try
        {   do
            {   doc.insertString(dot++, " ", blueText);
            }
            while ((dot-afterPrompt)%8 != 0);
        }
        catch (BadLocationException e1)
        {}
    }
}

class PasteAction extends AbstractAction
{
    PasteAction()
    {   super(StyledEditorKit.pasteAction);
    }

    public void actionPerformed(ActionEvent e)
    {
// Actually if I go PASTE when not in input mode I think I still need to
// grab the stuff off the clipboard instantly. It is inserting it into
// the buffer that gets delayed.
        Clipboard cb;
        Transferable tr;
        String s = null;
        try
        {
            cb = getToolkit().getSystemClipboard();
            tr = cb.getContents(this);
            s = (String)tr.getTransferData(DataFlavor.stringFlavor);
        }
        catch (Exception e1)
        {   s = clipboardSubstitute;
        }
        if (s == null || s.length() == 0) return; // No more to do!
        int p = 0, q;
        ActionEvent e2;
        String s1;
        while ((q = s.indexOf('\n', p)) >= 0)
        {   s1 = s.substring(p, q); // up to but not including newline
            e2 = new ActionEvent(this, 0, s1);
            deferAction(insertPasted, e2);
            p = q+1;
        }
        s1 = s.substring(p, s.length()); 
        if (s1.length() != 0)
        {   e2 = new ActionEvent(this, 0, s1, 1); // 1 marks "no newline"!
            deferAction(insertPasted, e2);
        }
        if (state != INPUTMODE) return;
        performPendedActions();
    }
}

class HistoryUpAction extends AbstractAction
{
    HistoryUpAction()
    {   super("history-up");
    }

    public void actionPerformed(ActionEvent e)
    {
        if (state != INPUTMODE)
        {   deferAction(this, e);
            return;
        }
        if (historyIndex > 0)
        {   historyIndex--;
            String s = "";
            try
            {   s = (String)history.get(historyIndex);
            }
            catch (ArrayIndexOutOfBoundsException e1)
            {};
            caret.setDot(afterPrompt);
            caret.moveDot(doc.getLength());
            replaceSelection("");
            try
            {   doc.insertString(afterPrompt, s, blueText);
                caret.setDot(doc.getLength());
            }
            catch (BadLocationException e1)
            {}
        }
    }
}

class HistoryDownAction extends AbstractAction
{
    HistoryDownAction()
    {   super("history-down");
    }

    public void actionPerformed(ActionEvent e)
    {
        if (state != INPUTMODE)
        {   deferAction(this, e);
            return;
        }
        int n = history.size();
        if (historyIndex < n)
        {   historyIndex++;
            String s = "";
            try
            {   if (historyIndex < n)
                    s = (String)history.get(historyIndex);
            }
            catch (ArrayIndexOutOfBoundsException e1)
            {};
            caret.setDot(afterPrompt);
            caret.moveDot(doc.getLength());
            replaceSelection("");
            try
            {   doc.insertString(afterPrompt, s, blueText);
                caret.setDot(doc.getLength());
            }
            catch (BadLocationException e1)
            {}
        }
    }
}


String safe(String a)
{
    if (a == null) return "<null>";
    String r = "";
    for (int i=0; i<a.length(); i++)
    {   char c = a.charAt(i);
        int ic = (int)c;
        if (32 <= ic && ic < 127) r = r + c;
        else r = r + "\\x" + Integer.toHexString(ic);
    }
    return r;
}

}

// end of InputPane.java
