package cnp.ew.richtext;

import java.awt.*;
import java.util.*;
import java.io.*;

import cnp.ew.scrolling.*;
import cnp.ew.util.*;
import cnp.ew.misc.*;
import cnp.ew.converter.*;
import cnp.ew.button.*;
import cnp.ew.lightweight.*;

public class CpRichTextPane extends CpVariableCellSizeScrollable implements CpObserver, CpTimerUser
{
    // These two are same format, a vector of CpParagraphs
    Vector paragraphs;
    Vector clipboard;

    // These two must be updated on scroll
    int topParaNum;
    int topParaY;

    CpTextLocation cursor;  // Format is paraNum, offset
    CpTextLocation selectionStart;
    CpTextLocation selectionEnd;
    CpTextLocation anchor; // Used just in mouseDown and drag
    CpTextLocation dragCursor; // For dragging selected text

    Point cachedAnchorPoint; // Used to avoid finding the pixel point for the anchor, during a drag

    CpCharFormat pendingFormat; // When they set a format with no selection, this should be used for subsequent characters

    // These are used to signal, during an cursor changed event, whether the format or paragraph have changed
    CpCharFormat oldFormat;
    CpParagraph oldParagraph;

    Point cursorPoint = new Point(0, 0);  // Used to make sure arrowing up or down keeps the correct column

    // These are the "paper".  right - left = paperWidth
    int pageLeft;
    int pageRight = 495;  // What should this be initialized to?  Some kind of "8.5 x 11"?

    boolean showingSelection=true;  // Only show the selection if we have the focus, depending on flag
    boolean hideSelectionOnFocusLoss=false;

    boolean wrapToWidth = false;

    boolean cursorOn = false;
    Thread cursorThread;
    boolean doubleClicked;

    String fileName;
    boolean isDirty;

    Color selectionColor = Color.yellow;
    Color cursorColor = Color.black;

    boolean javaMode = false;

    boolean tabWarning = true;

    int savedCursorType;

    public CpRichTextPane(Vector newParagraphs)
    {
        initialize(newParagraphs);
    }

    void initialize(Vector newParagraphs)
    {
        paragraphs = newParagraphs;
        for (int i = 0; i < paragraphs.size(); i++) {
            CpParagraph para = getPara(i);
            para.addObserver(this);
        }

        topParaNum = 0;
        topParaY = 0;
        cursor = new CpTextLocation(0, 0);
        cursorPoint = new Point(0, 0);
        selectionStart = cursor;
        selectionEnd = cursor;
        selectionStart = new CpTextLocation(0, 0);
        selectionEnd = new CpTextLocation(0, 0);
        pageLeft = 5;

        isDirty = false;
    }

    public void setWrapWidth(int w)
    {
        pageRight = w;
    }

    public void newDocument(Object frame)
    {
        if (checkDirty()) {
            return;
        }

        Vector newParagraphs = new Vector();
	    CpParagraph para = new CpParagraph();
	    para.setText("");
        newParagraphs.addElement(para);
        initialize(newParagraphs);

        fileName = null;
        rewrap();
        enableDamageRepair(false);
        scrollTo(0, 0);
        getScroller().scrollableSized();
        enableDamageRepair(true);
        if (frame != null && (frame instanceof Frame)) {
            ((Frame)frame).setTitle("Document");
        }
    }

    /********************** PUBLIC API ***************************/

    public void setParagraphs(Vector newParagraphs)
    {
        initialize(newParagraphs);
        fileName = null;
        if (javaMode) {
             parseJava();
        }
        rewrap();
        getScroller().scrollableSized();
    }

    public Vector getParagraphs()
    {
        return paragraphs;
    }

    public void addParagraph(CpParagraph newPara)
    {
        paragraphs.addElement(newPara);
    }

    public void setWrapToWidth(boolean bool)
    {
        wrapToWidth = bool;
        repaint();
    }

    // This rewraps everything, in case the page size has changed, or if it has never been wrapped
    public void rewrap()
    {
        for (int i = 0; i < paragraphs.size(); i++) {
            getPara(i).rewrap(pageLeft, pageRight);
        }
    }

    public boolean isAnySelection()
    {
        return !selectionStart.equals(selectionEnd);
    }

    public int getParagraphNum()
    {
        return cursor.paraNum();
    }

    public int getLineNum()
    {
        // Inefficient for large numbers of paragraphs...

        int curParaNum = 0;
        int lineNum = 0;

        while (curParaNum < cursor.paraNum()) {
            lineNum += getPara(curParaNum).numLines();
            curParaNum++;
        }
        return lineNum + getPara(curParaNum).lineNumForOffset(cursor.offset());
    }

    public int getPageLeft()
    {
        return pageLeft;
    }

    public int getPageRight()
    {
        return pageRight;
    }

    public CpParagraph getCurrentParagraph()
    {
        return getPara(cursor.paraNum());
    }

    public CpCharFormat getCurrentFormat()
    {
        return getPara(cursor.paraNum()).formatForOffset(cursor.offset());
    }

    /*********************** UTILITY *******************************/

    CpParagraph getPara(int paraNum)
    {
        return (CpParagraph)paragraphs.elementAt(paraNum);
    }

    /*********************** COORDINATE CONVERSION ***************************/

    int visibleHeight()
    {
        return getScroller().size().height;
    }

    int paraNumForY(int y)
    {
        int curY = 0; //topParaY;
        int curParaNum = 0; // topParaNum;

        while (curParaNum < paragraphs.size()) {
            CpParagraph curPara = getPara(curParaNum);
            if (curY + curPara.getHeight() > y) {
                return curParaNum;
            }
            curY += curPara.getHeight();
            curParaNum++;
        }
        return paragraphs.size() - 1;
    }

    int yForParaNum(int paraNum)
    {
        if (paraNum < topParaNum) {
            return - 1;
        }
        int curY = topParaY;
        int curParaNum = topParaNum;
        int endY = topParaY + visibleHeight() + getPara(topParaNum).getHeight();

        while (curY < endY) {
            if (paraNum == curParaNum) {
                return curY;
            }
            curY += getPara(curParaNum).getHeight();
            curParaNum++;
        }
        return -1; // Not a visible paragraph
    }

    int xForLoc(CpTextLocation loc)
    {
        int x = lineStartX(loc);
        return x + getPara(loc.paraNum()).xForOffset(loc.offset());
    }

    int yForLoc(CpTextLocation loc)
    {
        int paraY = yForParaNum(loc.paraNum());
        CpParagraph para = getPara(loc.paraNum());
  //      System.out.println(" para = " + para + " returning = " + paraY + para.yForLineNum(para.lineNumForOffset(loc.offset())));
        return paraY + para.yForLineNum(para.lineNumForOffset(loc.offset()));
    }


    int lineStartX(CpTextLocation loc)
    {
        CpParagraph para = getPara(loc.paraNum());
        return para.xForLineNum(para.lineNumForOffset(loc.offset()));
    }

    Point pointForLoc(CpTextLocation loc)
    {
        // Could be more efficient, both xFOrLoc (in lineStartX) and yForLoc call para.lineNumForOffset(..)
        return new Point(xForLoc(loc), yForLoc(loc));
    }

    /******************* MISC?  */

    public void textHasChanged()
    {
        // This method does not do scrollableSized, so we can do that more selectively
        // Note also:  sometimes this gets called twice (like paste which calls clearSelection which does this,
        //      and then calls textHasChanged directly.  Slightly inefficient but probably not that bigga deal.
        if (javaMode) {
             parseJava();
        }
        isDirty = true;
    }

    void parseJava()
    {
        if (!javaMode) {
            return;
        }
        boolean isMultiLine = false;
        for (int i = 0; i < paragraphs.size(); i++) {
            getPara(i).parseJava(isMultiLine);
            isMultiLine = getPara(i).getIsMultiLineJava();
        //    System.out.println("Parsed para: " + i + " isMultiline = " + isMultiLine);
        }
    //    for (int i = 0; i < paragraphs.size(); i++) {
    //        getPara(i).printDataStructures();
    //    }
    }

    /**********************   SCROLLING INFO   *******************************/

    //  Vertical is ignored (variable height lines)
    public Dimension getCellSize()
    {
    	return new Dimension(50, 0);
    }

    public void setLocationInCells(Point p)
    {
        super.setLocationInCells(p);

        // Must update topParaNum and topParaY

        int curY = 0;
        int curParaNum = 0;
        int curLine = getPara(0).numLines();

        while (p.y >= curLine) {
            curY += getPara(curParaNum).getHeight();
            curParaNum++;
            curLine += getPara(curParaNum).numLines();
        }
        topParaNum = curParaNum;
        topParaY = curY;
    }

    public Dimension getSizeInCells()
    {
        // Should probably be sped up?  numLines cached?

        int numLines = 0;
        for (int i = 0; i < paragraphs.size(); i++) {
            numLines += getPara(i).numLines();
        }
        return new Dimension((pageRight - pageLeft) / getCellSize().width, numLines);
    }

    public int getCellHeight(int lineNum)
    {
        int numLines = 0;
        CpParagraph para = null;
        int lineNumInPara = 0;

        for (int i = 0; i < paragraphs.size(); i++) {
            para = getPara(i);
            numLines += para.numLines();
            if (numLines > lineNum) {
                lineNumInPara = para.numLines() - (numLines - lineNum);
                break;
            }
        }
        return para.getLine(lineNumInPara).getHeight();
    }


    /**********************   PAINTING   *******************************/

    public void fillSelection(Graphics g, CpTextLocation first, CpTextLocation last, Rectangle clipRect)
    {
        // Could be spead up.  The first line look up is already done
        // in pointForLoc(selStart).  Somehow combine those.
        // Also, USE THE CLIPRECT.  Save some time.


        if (first.equals(last) || !showingSelection) {
            return;
        }

        // Avoid one lookup in drag select, since one point stays the same (the anchor)
        Point start, end;
        if (cachedAnchorPoint != null) {
            if (first.equals(anchor)) {
                start = cachedAnchorPoint;
                end = pointForLoc(last);
            } else {
                start = pointForLoc(first);
                end = cachedAnchorPoint;
            }
        } else {
            start = pointForLoc(first);
            end = pointForLoc(last);
        }

        CpParagraph para = getPara(selectionStart.paraNum());
        int lineNum = para.lineNumForOffset(selectionStart.offset());

        g.setColor(selectionColor);
        if (start.y == end.y) {   // One line selection
            g.fillRect(start.x, start.y, end.x - start.x, para.getLine(lineNum).getHeight());
        } else {
            int lineStartX = start.x;
            int curY = start.y;
            int curParaNum = selectionStart.paraNum();
            while (true) {
                CpLine line = para.getLine(lineNum);
                int width;
                if (curY == start.y) { // first line
                    width = line.getWidth() - (start.x - para.xForLineNum(lineNum));
                } else if (curY == end.y) { //last line
                    width = end.x - lineStartX;
                } else {
                    width = line.getWidth();
                }
                g.fillRect(lineStartX, curY, width,  line.getHeight());
                curY += line.getHeight();

                if (curY > end.y) {
                    return;
                }

                lineNum++;
                if (para.numLines() <= lineNum) { // get next para
                    curParaNum++;
                    para = getPara(curParaNum);
                    lineNum = 0;
                }
                lineStartX = para.xForLineNum(lineNum);
            }
        }

	}

    public void paint(Graphics g, Rectangle clipRect)
    {
        // This would cause the pagesize to size when the window is sized
        if (wrapToWidth) {
          if (pageRight != getScroller().size().width - 10) {
            pageRight = getScroller().size().width - 10;
            rewrap();
          }
        }


        g.setColor(getScroller().getBackground());
        g.fillRect(clipRect.x, clipRect.y, clipRect.width, clipRect.height);

        fillSelection(g, selectionStart, selectionEnd, clipRect);

        int firstParaNum = paraNumForY(clipRect.y);

        int curY = yForParaNum(firstParaNum);

//        System.out.println("Paint in pane, firstParaNum = " + firstParaNum + " curY = " + curY + " clipY = " + clipRect.y);

        // Temporary fix for moving lcs.  Should only draw either twice the height of visible or just the height we've added...
        boolean drawToEnd = false;
        if (clipRect.equals(getVisibleRect())) {
            drawToEnd = true;
        }

        for (int p = firstParaNum; p < paragraphs.size(); p++) {
            curY = getPara(p).paintFrom(g, curY, clipRect);
            if (!drawToEnd && curY > clipRect.y + clipRect.height) {
                break;
            }
        }

		if (cursorOn || dragCursor != null) {
		    CpTextLocation tempCursor;
		    if (dragCursor != null) {
		        tempCursor = dragCursor;
		    } else {
		        tempCursor = cursor;
		    }
            Point cursorPoint = pointForLoc(tempCursor);
		    g.setColor(cursorColor);
		 //  System.out.println("curosr point = " + cursorPoint);
		    CpCharFormat curFormat = getCurrentFormat();
		    int cursorHeight = curFormat.getHeight();
		    g.fillRect(cursorPoint.x, cursorPoint.y + getLineHeight(tempCursor) - (getPara(cursor.paraNum()).lineForOffset(cursor.offset()).getDescent() - curFormat.getDescent()) - cursorHeight, 1, cursorHeight);
		}
    }

	/*************************** MORE PUBLIC API, cut/copy/paste, insert, etc. **********/

    public void menuCut()
    {
        menuCopy();
        clearSelection(true);
    }

    public void menuCopy()
    {
        CpParagraph para;

        if (selectionStart.equals(selectionEnd)) {
            return;
        }
        clipboard = new Vector();
        para = getPara(selectionStart.paraNum()).getCopy();
        para.delete(0, selectionStart.offset());
        if (selectionStart.paraNum() == selectionEnd.paraNum()) {
            para.delete(selectionEnd.offset() - selectionStart.offset(), para.length() - 1);
            clipboard.addElement(para);
        } else {
            clipboard.addElement(para);
            for (int i = selectionStart.paraNum() + 1; i <= selectionEnd.paraNum() - 1; i++) {
                clipboard.addElement(getPara(i).getCopy());
             //   paragraphs.removeElementAt(selectionStart.paraNum() + 1);
            }
            para = getPara(selectionEnd.paraNum()).getCopy();
            para.delete(selectionEnd.offset(), para.length() - 1);
            clipboard.addElement(para);
        }
    }

    public void menuPaste()
    {
        if (clipboard == null || clipboard.isEmpty()) {
            return;
        }
        clearSelection(false);
        CpParagraph para, last;
        para = getPara(cursor.paraNum());
        int newOffset;
        if (clipboard.size() == 1) {
            para.insertParagraph(cursor.offset(), last = ((CpParagraph)clipboard.elementAt(0)).getCopy());
            newOffset = cursor.offset() + last.length() - 1;
        } else {
            last = para.splitParagraph(cursor.offset());
            para.appendParagraph(((CpParagraph)clipboard.elementAt(0)).getCopy());
            for (int i = 1; i < clipboard.size(); i++) {
                paragraphs.insertElementAt(para = ((CpParagraph)clipboard.elementAt(i)).getCopy(), cursor.paraNum() + i);
            }
            newOffset = para.length() - 1;
            para.appendParagraph(last);
        }
        setCursor(new CpTextLocation(cursor.paraNum() + clipboard.size() - 1, newOffset), true, true);
        textHasChanged();
        getScroller().scrollableSized();
    }

    public boolean menuFind(String s)
    {
        for (int i = cursor.paraNum(); i < paragraphs.size(); i++) {
            CpParagraph para = getPara(i);
            int offset = para.find(s, cursor.offset());
            if (offset > 0) {
                selectionStart = new CpTextLocation(i, offset);
                selectionEnd = new CpTextLocation(i, offset + s.length());
                setCursor(selectionEnd, true, true);
                repaint();
                return true;
            }
        }
        return false;
    }

    public void menuInsertDate()
    {
        CpDateToStringConverter converter = new CpDateToStringConverter("dddd, mmmm d, yyyy");
        String dateString = converter.convert(new Date());
        CpParagraph datePara = new CpParagraph(dateString);
        clipboard = new Vector();
        clipboard.addElement(datePara);
        menuPaste();
    }

    public void setJavaMode(boolean isJavaMode)
    {
        javaMode = isJavaMode;
    }


    public void setMargins(int left, int right, int first)
    {
       // System.out.println("In set margins");
        for (int i = selectionStart.paraNum(); i <= selectionEnd.paraNum(); i++) {
            CpParagraph para = getPara(i);
            int numLinesInPara = para.numLines();
            para.setMargins(left, right, first);
          //  System.out.println("wraping para: " + i);
            para.rewrap(pageLeft, pageRight);
            if (selectionStart.paraNum() != selectionEnd.paraNum() || para.numLines() != numLinesInPara) { // A line was added or removed, redisplay...
                damage();
            } else {
                damageLinesFrom(new CpTextLocation(cursor.paraNum(), 0), new CpTextLocation(cursor.paraNum(), para.length() - 1));
            }
        }
        repairDamage();
    }

    public void setHorizontalAlignment(int alignment)
    {
        for (int i = selectionStart.paraNum(); i <= selectionEnd.paraNum(); i++) {
            CpParagraph para = getPara(i);
            para.setHorizontalAlignment(alignment);
        }
        repaint();
        notifyObservers();
    }

    public void setBulletMode(boolean isBulletMode)
    {
        // 14 is definitely a magic number hack.  Use para.bulletIndentDefault or something like that...

        for (int i = selectionStart.paraNum(); i <= selectionEnd.paraNum(); i++) {
            CpParagraph para = getPara(i);
            para.setBulletMode(isBulletMode);
            if (para.getLeftMargin() == 0 && isBulletMode) {
                para.setMargins(14, para.getRightMargin(), para.getFirstIndent());
            }
            if (para.getLeftMargin() == 14 && !isBulletMode) {
                para.setMargins(0, para.getRightMargin(), para.getFirstIndent());
            }
        }
        repaint();
    }


    public void update(CpObservable o, int facet, Object arg)
    {
    }

    public Dimension preferredSize()
    {
	    // Arbitrary...
	    return new Dimension(400, 300);
    }

    CpTextLocation locForPoint(int x, int y)
    {
        int paraNum = paraNumForY(y);
        int localY = y - yForParaNum(paraNum);
        int lineNum = getPara(paraNum).lineNumForY(localY);
        int offset = getPara(paraNum).offsetForX(lineNum, x - getPara(paraNum).xForLineNum(lineNum));
        return new CpTextLocation(paraNum, offset);
    }

    boolean inSelection(CpTextLocation loc)
    {
        if (loc.paraNum() > selectionStart.paraNum() && loc.paraNum() < selectionEnd.paraNum()) {
            return true;
        } else if (loc.paraNum() == selectionStart.paraNum() && loc.paraNum() == selectionEnd.paraNum()) {
            return loc.offset() >= selectionStart.offset() && loc.offset() < selectionEnd.offset();
        } else {
            return (loc.paraNum() == selectionStart.paraNum() && loc.offset() >= selectionStart.offset()) ||
                (loc.paraNum() == selectionEnd.paraNum() && loc.offset() < selectionEnd.offset());
        }
    }

    public boolean mouseDown(Event evt, int x, int y)
    {
        oldFormat = getCurrentFormat();
        oldParagraph = getCurrentParagraph();

        CpTextLocation loc = locForPoint(x, y);

        doubleClicked = evt.clickCount == 2 && loc.equals(cursor);
        CpTimer.getDefaultTimer().unregister(this);
        cursorOn = false;

        if (doubleClicked) {
              Point selectionRange = getPara(cursor.paraNum()).getWordAtOffset(cursor.offset());
              selectionStart = new CpTextLocation(cursor.paraNum(), selectionRange.x);
              selectionEnd = new CpTextLocation(cursor.paraNum(), selectionRange.y);
              setCursor(selectionEnd, true, true);
              damageLinesFrom(cursor, cursor);
        } else {
            if (!selectionStart.equals(selectionEnd)) {
                // For now, COMMENTING out drag text code.  Pretty much works
             //   if (inSelection(loc)) {
               //     dragCursor = loc;
                 //   return false;
              //  } else {
                    damage();
               // }
            } else {
                damageLinesFrom(cursor, cursor);
            }
            if (evt.shiftDown()) {
                if (cursor.equals(selectionStart)) {
                    anchor = selectionEnd;
                    selectionStart = cursor = loc;
                } else {
                    anchor = selectionStart;
                    selectionEnd = cursor = loc;
                }
                normalizeSelection();
                damage();
            } else {
                selectionStart = selectionEnd = cursor = anchor = loc;
            }
            cachedAnchorPoint = pointForLoc(anchor);
        }
        repairDamage();
        return true;
    }

    void normalizeSelection()
    {
        if (selectionEnd.isBefore(selectionStart)) {
            CpTextLocation temp = selectionStart;
            selectionStart = selectionEnd;
            selectionEnd = temp;
        }
    }

    int getLineHeight(CpTextLocation loc)
    {
        CpParagraph para = getPara(loc.paraNum());
        return (para.lineForOffset(loc.offset())).getHeight();
    }

    // Issue:  SEE Scroller, also.  Should this just be done by layout()?
    public void scrollerSized(Dimension newSize)
    {
        recomputeSize();
    }


    void damageLinesFrom(CpTextLocation loc1, CpTextLocation loc2)
    {
        // SPEEDUP: This should only bother damaging visible lines

        CpTextLocation start = loc1;
        CpTextLocation end = loc2;
        if (loc2.isBefore(loc1)) {
            start = loc2;
            end = loc1;
        }
        int startY = yForLoc(start);
        int endY = yForLoc(end);

    //    System.out.println("my bounds = " + boundsGlobal());
    //    System.out.println("Damaging rect: " + new Rectangle(0, startY, 10000, (endY + getLineHeight(end)) - startY));
      //  System.out.println("My scroller rect = " + getScroller().boundsGlobal());
//        System.out.println("Intersect = " + (new Rectangle(0, startY, 10000, (endY + getLineHeight(end)) - startY)).intersection(getScroller().boundsGlobal()));
        damage(new Rectangle(0, startY, 10000, (endY + getLineHeight(end)) - startY));
    }

    void setCursor(CpTextLocation newCursor, boolean savePoint, boolean notify)
    {
        CpCharFormat oldFormat = getCurrentFormat();
        CpParagraph oldParagraph = getCurrentParagraph();

        pendingFormat = null;

        cursor = newCursor;

        if (selectionStart.equals(selectionEnd)) {
            selectionStart = selectionEnd = cursor;
        }
//        makeCellVisible(0, getLineNum());
        Point newCursorPoint = pointForLoc(cursor);
        if (savePoint) {
            cursorPoint = pointForLoc(cursor);
        } else {
            cursorPoint = new Point(cursorPoint.x, newCursorPoint.y);
        }
        if (notify) {
            notifyObservers(CURSOR_MOVED, new Boolean(!oldFormat.equals(getCurrentFormat()) || oldParagraph != getCurrentParagraph()));
        }
    }


    public boolean mouseDrag(Event evt, int x, int y)
    {
        if (doubleClicked) {
            return true;
        }

        CpTextLocation newCursor = locForPoint(x, y);
        if (dragCursor != null) {
            damageLinesFrom(dragCursor, newCursor);
            dragCursor = newCursor;
            repairDamage();
            return true;
        }
        if (!cursor.equals(newCursor)) {
    //    System.out.println("Damaging lines from: " + selectionEnd + " to " + cursor);
            CpTextLocation oldCursor = cursor;
            cursor = newCursor;
         //   makeCellVisible(0, getLineNum());
            damageLinesFrom(oldCursor, cursor);
            selectionStart = anchor;
            selectionEnd = cursor;
            normalizeSelection();
            repairDamage();
        }
        return true;
    }

    public boolean mouseUp(Event evt, int x, int y)
    {

        CpTimer.getDefaultTimer().register(this, 500);
        cachedAnchorPoint = null;
        if (doubleClicked) {
            doubleClicked = false;
        } else {
            CpTextLocation newCursor = locForPoint(x, y);
            setCursor(newCursor, true, false);
        }
        if (dragCursor != null) {
            enableDamageRepair(false);
            menuCut();
            setCursor(locForPoint(x, y), true, false);
            selectionStart = selectionEnd = cursor;
            dragCursor = null;
         //   System.out.println("About to paste at: " + cursor + " sel st " + selectionStart);
            enableDamageRepair(true);
            menuPaste();
        } else {
            damageLinesFrom(cursor, cursor);
            repairDamage();
        }
        notifyObservers(CURSOR_MOVED, new Boolean(oldFormat != getCurrentFormat() || oldParagraph != getCurrentParagraph()));
        return true;
    }

    public boolean mouseEnter(Event evt, int cellX, int cellY)
    {
        savedCursorType = getFrame().getCursorType();
        setCursor(Frame.TEXT_CURSOR);
        return false;
    }

    public boolean mouseExit(Event evt, int cellX, int cellY)
    {
        setCursor(savedCursorType);
        return false;
    }

    public long timerEvent(int timerType)
    {
        cursorOn = !cursorOn;
        damageLinesFrom(cursor, cursor);
        repairDamage();
        return 500;
    }

    public boolean gotFocus()
    {
        CpTimer.getDefaultTimer().register(this, 500);
        showingSelection = true;
        return false;
    }

    public boolean usesFocus()
    {
        return true;
    }

    public boolean wantsLeftAndRightArrows()
    {
        return true;
    }

    public boolean wantsCr()
    {
        return true;
    }

    public boolean wantsTab()
    {
        return true;
    }

    public boolean lostFocus()
    {
        CpTimer.getDefaultTimer().unregister(this);
        if (cursorOn) {
            cursorOn = false;
            damageLinesFrom(cursor, cursor);
            repairDamage();
        }

        if (hideSelectionOnFocusLoss) {
            showingSelection = false;
            repaint();
        }
        return false;
    }

    public boolean keyUp(Event e, int key)
    {
        CpTimer.getDefaultTimer().register(this, 500);
        return true;
    }

    CpTextLocation getOffsetMoved(CpTextLocation loc, int delta)
    {
        int newOffset = loc.offset() + delta;
        if (newOffset < 0) {
            if (loc.paraNum() > 0) {
                return new CpTextLocation(loc.paraNum() - 1, getPara(loc.paraNum() - 1).length() - 1);
            } else {
                return loc;
            }
        }
        CpParagraph para = getPara(loc.paraNum());
        if (newOffset >= para.length()) {
            if (loc.paraNum() < paragraphs.size() - 1) {
                return new CpTextLocation(loc.paraNum() + 1, 0);
            } else {
                return loc;
            }
        }
        return new CpTextLocation(loc.paraNum(), newOffset);
    }

    public boolean clearSelection(boolean repairDamage)
    {
        if (selectionStart.equals(selectionEnd)) {
            return false;
        }
        if (selectionStart.paraNum() == selectionEnd.paraNum()) {
            getPara(selectionStart.paraNum()).delete(selectionStart.offset(), selectionEnd.offset());
        } else {
            CpParagraph firstPara = getPara(selectionStart.paraNum());
         //   System.out.println("First para:");
        //    firstPara.printDataStructures();
            firstPara.delete(selectionStart.offset(), firstPara.length() - 1);
            CpParagraph lastPara = getPara(selectionEnd.paraNum());
            lastPara.delete(0, selectionEnd.offset());
//            System.out.println("First para:");
     //       firstPara.printDataStructures();
  //          System.out.println("Last para:");
      //      lastPara.printDataStructures();
            firstPara.appendParagraph(lastPara);
            for (int i = selectionStart.paraNum() + 1; i <= selectionEnd.paraNum(); i++) {
                paragraphs.removeElementAt(selectionStart.paraNum() + 1);
            }
            //System.out.println("NEW First para:");
            //firstPara.printDataStructures();
        }
        selectionEnd = cursor = selectionStart;
        textHasChanged();
        if (repairDamage) {
            getScroller().scrollableSized();
        } else {
            enableDamageRepair(false);
            getScroller().scrollableSized();
            enableDamageRepair(true);
        }
        return true;
    }

    public boolean keyDown(Event e, int key)
    {

        CpTimer.getDefaultTimer().unregister(this);
        cursorOn = true;

        CpTextLocation oldCursor = cursor;
          //  System.out.println("Key = " + key);
        switch (key) {
        case 3:
            menuCopy();
            break;
        case 24:
            menuCut();
            break;
        case 22:
            menuPaste();
            break;
        case 9:
            if (tabWarning) {
              //  CpPrompter p = new CpPrompter("JavaPad", "Full tab support was not completed in time for this demo.  We will insert spaces instead.  This warning will not appear again.");
             //   p.show();
              //  tabWarning = false;
            }
            clipboard = new Vector();
            clipboard.addElement(new CpParagraph("    "));
            menuPaste();

            break;
        case Event.RIGHT:
        case Event.LEFT:
        case Event.UP:
        case Event.DOWN:
            if (key == Event.RIGHT) {
                setCursor(getOffsetMoved(cursor, 1), true, true);
            } else if (key == Event.LEFT) {
                setCursor(getOffsetMoved(cursor, -1), true, true);
            } else if (key == Event.UP) {
                setCursor(locForPoint(cursorPoint.x, cursorPoint.y - 1), false, true);
            } else {
                setCursor(locForPoint(cursorPoint.x, cursorPoint.y + getLineHeight(cursor)), false, true);
            }
            if (e.shiftDown()) {
                if (oldCursor.equals(selectionStart)) {
                    selectionStart = cursor;
                } else {
                    selectionEnd = cursor;
                }
                normalizeSelection();
            } else {
                selectionStart = selectionEnd = cursor;
            }
            cursorOn = true;
            damageLinesFrom(oldCursor, cursor);
            repairDamage();
            break;
        case Event.HOME:
            setCursor(new CpTextLocation(0, 0), true, true);
            selectionStart = selectionEnd = cursor;
            damageLinesFrom(oldCursor, cursor);
            repairDamage();
            break;
        case Event.END:
            setCursor(new CpTextLocation(paragraphs.size() - 1, getPara(paragraphs.size() - 1).length() - 1), true, true);
            selectionStart = selectionEnd = cursor;
            damageLinesFrom(oldCursor, cursor);
            repairDamage();
            break;
        case 10 : // CR
            CpParagraph para, newPara;
            para = getPara(cursor.paraNum());
            newPara = para.splitParagraph(cursor.offset());
            paragraphs.insertElementAt(newPara, cursor.paraNum() + 1);
            setCursor(getOffsetMoved(cursor, 1), true, true);
            textHasChanged();
            getScroller().scrollableSized();
            break;
        case 127: // Delete
            if (clearSelection(true)) {
                return true;
            }
            setCursor(getOffsetMoved(cursor, 1), false, false);
            if (oldCursor.equals(cursor)) {  // if so, we must have been at the end, so don't do anything
                return true;
            }
            oldCursor = cursor;
            // else, just fall through to backspace
        case 8: // Backspace
            if (!clearSelection(true)) {
                if (cursor.offset() == 0) {  // start of a paragraph
                    // Hack, kinda
                    setCursor(getOffsetMoved(cursor, -1), true, true);
                    selectionEnd = oldCursor;
                    clearSelection(false);
                    getScroller().scrollableSized();
                } else {
                    para = getPara(cursor.paraNum());
                    int numLinesInPara = para.numLines();
                    int paraHeight = para.getHeight();
                    int oldNumChars = para.getNumCharsInLineWithOffset(cursor.offset());
                    char deletedChar = para.charAt(cursor.offset() - 1);
                    para.delete(cursor.offset() - 1, cursor.offset());
                    setCursor(getOffsetMoved(cursor, -1), true, true);
                    textHasChanged();
                    // This is bad.  I'm redisplaying anytime the paragraph wraps. even if a whole line is not deleted.  Should do what isertChar does (return the damagedLines)
                    if (para.numLines() != numLinesInPara || para.getHeight() != paraHeight || para.getNumCharsInLineWithOffset(cursor.offset) != oldNumChars - 1) { // A line was added or removed, redisplay...
                        getScroller().scrollableSized();
                    } else {
                        if (javaMode && (deletedChar == '*' || deletedChar == '/')) {
                            damage();
                        } else {
                            damageLinesFrom(cursor, cursor);
                        }
                        repairDamage();
                    }
                }
            }
            break;
        default:
            if (key > 30 && key < 1000) {  // Avoid control chars (I just made up 30.  WhAT IS IT REALLY?) is there some isMeta or isFunction?
                clearSelection(false);
                para = getPara(cursor.paraNum());
                int numLinesInPara = para.numLines();
                int paraHeight = para.getHeight();
                Point damagedLines = para.insertChar(cursor.offset(), (char)key);
                if (pendingFormat != null) {
                    selectionStart = cursor;
                    selectionEnd = getOffsetMoved(cursor, 1);
                    enableDamageRepair(false);
                    mergeFormat(pendingFormat);
                    pendingFormat = null;
                    selectionStart = selectionEnd;
                    enableDamageRepair(true);
                }
                setCursor(getOffsetMoved(cursor, 1), true, true);
                if (javaMode && key != '*' && key != '/') {
                    boolean isMultiLine = cursor.paraNum() != 0 && getPara(cursor.paraNum() - 1).getIsMultiLineJava();
                    para.parseJava(isMultiLine);
                    isDirty = true;
                } else {
                    if (javaMode) {
                        damage();
                    }
                    textHasChanged();
                }
          //      System.out.println("Inserted " + key + " damagedLines are " + damagedLines);
                if (para.numLines() != numLinesInPara || para.getHeight() != paraHeight ) { // A line was added or removed, redisplay...
                    getScroller().scrollableSized();
                } else {
                    damageLinesFrom(new CpTextLocation(cursor.paraNum(), damagedLines.x), new CpTextLocation(cursor.paraNum(), damagedLines.y));
                    repairDamage();
                }
            }
        }

        return true;
    }

    public void insertImage(Image image)
    {
        CpImageFormat format = new CpImageFormat(image);
        insertCharWithFormat((char)0, format);
    }

    public void insertLc(CpLightweightComponent lc)
    {
        CpLcFormat format = new CpLcFormat(this, lc);
        insertCharWithFormat((char)0, format);
    }

    public void insertCharWithFormat(char c, CpCharFormat f)
    {
        getPara(cursor.paraNum()).insertChar(cursor.offset(), c);
        textHasChanged();
        selectionStart = cursor;
        selectionEnd = new CpTextLocation(cursor.paraNum(), cursor.offset() + 1);
        enableDamageRepair(false);
        addFormat(f);

        setCursor(getOffsetMoved(cursor, 1), true, true);
        selectionStart = selectionEnd = cursor;
        enableDamageRepair(true);
        getScroller().scrollableSized();
        isDirty = true;
    }

    public void mergeFormat(CpCharFormat format)
    {
        addOrMergeFormat(true, format);
    }

    public void addFormat(CpCharFormat format)
    {
        addOrMergeFormat(false, format);
    }

    public void addOrMergeFormat(boolean merge, CpCharFormat format)
    {
        // This is very similar to clearSelection...

        if (selectionStart.equals(selectionEnd)) {
            if (pendingFormat != null) {
                pendingFormat = pendingFormat.merge(format);
            } else {
                pendingFormat = format;
            }
            return;
        }
        if (selectionStart.paraNum() == selectionEnd.paraNum()) {
            getPara(selectionStart.paraNum()).addOrMergeFormat(merge, selectionStart.offset(), selectionEnd.offset(), format);
        } else {
            CpParagraph para = getPara(selectionStart.paraNum());
            para.addOrMergeFormat(merge, selectionStart.offset(), para.length() - 1, format);
            para = getPara(selectionEnd.paraNum());
            para.addOrMergeFormat(merge, 0, selectionEnd.offset(), format);
            for (int i = selectionStart.paraNum() + 1; i <= selectionEnd.paraNum() - 1; i++) {
                para = getPara(i);
                para.addOrMergeFormat(merge, 0, para.length() - 1, format);
            }
        }
        getScroller().scrollableSized();
    }


    /*********************  FILE SAVE/LOAD ***************************/

	public boolean checkDirty()
	{
        if (isDirty) {
             //   SAVE CHANGES? DIALOG WOULD GO HERE.  Or signal event.
        }
        return false;
        // return true if cancel?
    }

    public void openDocument(Object frame) throws IOException
    {
        if (checkDirty()) {
            return;
        }

        FileDialog dialog = new FileDialog((Frame)frame, "Open", FileDialog.LOAD);
        dialog.setDirectory("/My Documents");
        dialog.show();
      //  System.out.println("Name is " + dialog.getFile());
        fileName = dialog.getFile();
      //  System.out.println("Filename length() = " + fileName.length());
        //   fileName = fileName.substring(0, 8);

        if (frame != null) {
            ((Frame)frame).setTitle(fileName);
        }
        RandomAccessFile file = new RandomAccessFile(fileName, "rw");

        file.seek(0);
   //     System.out.println("File length = " + file.length() + " filepointer: " + file.getFilePointer());
        int sig1 = file.readInt();
        int sig2 = file.readInt();
        int sig3 = file.readInt();
        Vector newParagraphs = new Vector();
        if (sig1 == 99 && sig2 == 98 && sig3 == 97) {
            pageLeft = file.readInt();
            pageRight = file.readInt();
            int numParas = file.readInt();
            for (int i = 0; i < numParas; i++) {
                newParagraphs.addElement(new CpParagraph(file));
            }
            initialize(newParagraphs);
        } else {  // TEXT FILE
            file.close();
             file = new RandomAccessFile(fileName, "rw");
            String line;
            while ((line = file.readLine()) != null) {
              //  System.out.println("Read line: " + line);
                CpParagraph para = new CpParagraph();
                int lastTab = line.lastIndexOf('\t');
                if (lastTab < 0) {
                    para.setText(line);
                } else {
                    para.setText(line.substring(lastTab));
                }
                newParagraphs.addElement(para);
            }
        //    System.out.println("END OF FILE");
            initialize(newParagraphs);
            String temp = fileName.substring(fileName.indexOf("."), fileName.length());
            if (fileName.substring(fileName.indexOf("."), fileName.length()).equals(".java")) {
                javaMode = true;
                parseJava();
            }
        }
        file.close();
        rewrap();
        repaint();
    }



    public void saveDocument(Object frame) throws IOException
    {
        if (fileName == null) {
            saveDocumentAs(frame);
        }
        save();
    }

    public void saveDocumentAs(Object frame) throws IOException
    {
        FileDialog dialog = new FileDialog((Frame)frame, "Save As", FileDialog.SAVE);
        dialog.setDirectory("/My Documents");
        dialog.show();
        if (dialog.getFile() != null) {
            fileName = dialog.getFile().substring(0, dialog.getFile().length() - 4);
            if (frame != null) {
                ((Frame)frame).setTitle(fileName);
            }
            save();
        }
    }

    void save() throws IOException
    {
        isDirty = false;
        RandomAccessFile file = new RandomAccessFile("boo.jp", "rw");
        // Kind of dumb way to write a "signature"
        file.writeInt(99);
        file.writeInt(98);
        file.writeInt(97);

        file.writeInt(pageLeft);
        file.writeInt(pageRight);
        file.writeInt(paragraphs.size());
        for (int i = 0; i < paragraphs.size(); i++) {
            getPara(i).writeTo(file);
        }
        file.close();
    }

}
