package cnp.ew.text;

import java.awt.*;
import cnp.ew.util.*;
import cnp.ew.converter.*;
import cnp.ew.lightweight.*;
import cnp.ew.properties.*;

public class CpEntryFieldLc extends CpAbstractLc
implements CpTimerUser, CpEditor
{

    // Determines the task to perform when the timer goes off.
    static final int DRAW_CURSOR=0;
    static final int DRAG_MOUSE=1;

    static final int cursorInterval = 500;
    static final int mouseDragInterval = 100;

    static String clipboard;
    static String tabSpaces = "    ";

    CpEntryFieldTextModel textModel;

    CpToStringConverter inputConverter;
    CpFromStringConverter outputConverter;

    Color selectionBackColor = new Color(0, 0, 128);
    Color selectionForeColor = Color.white;
    int horizontalAlignment = ALIGN_LEFT;
    int verticalAlignment = ALIGN_TOP;

    boolean showingSelection=true;
    boolean hideSelectionOnFocusLoss=true;
    boolean selectAllOnGettingFocus=false;
    boolean sizeToFit=false;
    boolean hasChanged = false;
    boolean hasChangedSinceGotFocus = false;
    boolean wantsCr=false;

    int savedCursorType;

    /**
     * This mechanism is used to allow the first mouse click on a spin field
     * to select all, without setting the cursor.  Used for the option of selectAllOnGettingFocus.
     */
    boolean absorbMouseClick = false;

    boolean cursorOn = false;
    int cursorPos = 0;
    int selEnd = 0;
    int selStart = 0;
    int anchor;
    int scrollIndexOffset=0;

    // Used to avoid mousedrag after doubleclick selecting.
    boolean doubleClicked;
    // Used to determine the width the entryfield should be (maxChars * fontwidth) for preferredSize.
    int maxChars = 5;

    public CpEntryFieldLc(String s)
    {
        this();
        setText(s);
    }


    public CpEntryFieldLc()
    {
        super();
        textModel = new CpEntryFieldTextModel();
        setFont(CpFonts.dialogPlainEight());
    }

    synchronized public void setHorizontalAlignment(int newAlignment)
    {
        horizontalAlignment = newAlignment;
        repaintClientArea();
    }

    public int getHorizontalAlignment()
    {
        return horizontalAlignment;
    }

    public void setVerticalAlignment(int newAlignment)
    {
        verticalAlignment = newAlignment;
    }

    public int getVerticalAlignment()
    {
        return verticalAlignment;
    }

    public void setSelectAllOnGettingFocus(boolean shouldSelectAll)
    {
        selectAllOnGettingFocus = shouldSelectAll;
    }

    public boolean getSelectAllOnGettingFocus()
    {
        return selectAllOnGettingFocus;
    }

    public void setHideSelectionOnFocusLoss(boolean shouldHide)
    {
        hideSelectionOnFocusLoss = shouldHide;
    }

    public boolean getHideSelectionOnFocusLoss()
    {
        return hideSelectionOnFocusLoss;
    }

    public void setObject(Object object)
    {
        setText(getInputConverter().convert(object));
    }

    public Object getObject()
    {
        return getOutputConverter().convert(getText());
    }

    public void setInputConverter(CpToStringConverter converter)
    {
        inputConverter = converter;
    }

    public void setMaxChars(int newMaxChars)
    {
        maxChars = newMaxChars;
    }

    public int getMaxChars()
    {
        return maxChars;
    }

    public CpToStringConverter getInputConverter()
    {
        if (inputConverter == null) {
            inputConverter = new CpStringToStringConverter();
        }
        return inputConverter;
    }

    public void setOutputConverter(CpFromStringConverter converter)
    {
        outputConverter = converter;
    }

    public CpFromStringConverter getOutputConverter()
    {
        if (outputConverter == null) {
            outputConverter = new CpStringFromStringConverter();
        }
        return outputConverter;
    }


    public void setSelectionForeColor(Color color)
    {
        selectionForeColor = color;
    }

    public Color getSelectionForeColor()
    {
        return selectionForeColor;
    }

    public void setSelectionBackColor(Color color)
    {
        selectionBackColor = color;
    }

    public Color getSelectionBackColor()
    {
        return selectionBackColor;
    }

    synchronized public void selectAll()
    {
        anchor = selStart = 0;
        cursorPos = selEnd = textModel.getTextLength();
        repaintClientArea();
    }

    synchronized public void select(int from, int to)
    {
        anchor = selStart = from;
        cursorPos = selEnd = to;
        repaintClientArea();
    }

    public void beep()
    {
    }

    public Dimension preferredSize()
    {
        FontMetrics metrics = getFontMetrics();
        if (sizeToFit) {
            return borderedPreferredSize(new Dimension(
                metrics.charsWidth(textModel.chars, 0, textModel.length) + 2 * metrics.charWidth('W'),
                metrics.getHeight()
                ));
        } else {
            return borderedPreferredSize(new Dimension(metrics.charWidth('0') * maxChars, getFontMetrics().getHeight()));
        }
    }


    public String getText()
    {
        return textModel.getText();
    }

    synchronized public void setText(String s)
    {
        // Ted added 10/18
        if (getText().equals(s)) {
            return;
        }

        textModel.setText(s);
        hasChanged = false;
        cursorPos = selStart = selEnd = 0;
        scrollHome();
        repaintClientArea();
    }

    void doMouseDrag(int x, int y)
    {
        int i, newSelStart, newSelEnd;
        int width = size().width;
        FontMetrics fm = getFontMetrics();

        if (absorbMouseClick) {
            return;
        }

        if (doubleClicked) {
            return;
        }
        if (x > width) {
            cursorPos = Math.max(Math.min(textModel.length, iFor(width + 1) + 1), 0);
        } else if (x < 0) {
            cursorPos = Math.min(Math.max(0, iFor(0) - 1), textModel.length - 1);
        } else if (x < width && x > 0) {
            cursorPos = iFor(x);
        } else {
            return;
        }

        if (cursorPos > anchor) {
            newSelStart = anchor;
            newSelEnd = cursorPos;
        } else {
            newSelEnd = anchor;
            newSelStart = cursorPos;
        }

        //System.out.println("newSelStart: " + newSelStart + " newSelEnd: " + newSelEnd);

        if ((newSelStart != selStart) || (newSelEnd != selEnd)) {
            selStart = newSelStart;
            selEnd = newSelEnd;
            cursorOn = false;
            keepCursorVisible();
            repaintClientArea();
        }
    }


    synchronized public void cut()
    {
        copy();
        clearSelection();
        changingText();
    }

    synchronized public void copy()
    {
        if (selStart != selEnd) {
            clipboard = textModel.getSubstring(selStart, selEnd);
        }
    }

    synchronized public void paste()
    {
        if (clipboard != null) {
            clearSelection();
            textModel.insertString(clipboard, cursorPos);
            cursorPos += clipboard.length();
            changingText();
        }
    }

    public boolean mouseDrag(Event e, int x, int y)
    {
        doMouseDrag(x, y);
        return true;
    }

    public boolean mouseUp(Event e, int x, int y)
    {
        stopTimers();
        startCursorTimer();
        absorbMouseClick = false;
        doubleClicked = false;
        return true;
    }

    public boolean mouseDown(Event e, int x, int y)
    {
        int savedAnchor=0;
        doubleClicked = e.clickCount == 2;
        stopTimers();
        if (absorbMouseClick) {
            return true;
        }
        if (doubleClicked) {
            Point selectionRange = textModel.getWordSelectionHitBy(iFor(x));
            if (e.shiftDown()) {
                if (cursorPos == anchor) {
                    anchor = selStart = selectionRange.x;
                    cursorPos = selEnd = selectionRange.y;
                } else if (cursorPos > anchor) {
                    selEnd = cursorPos = selectionRange.y;
                } else {
                    selStart = cursorPos = selectionRange.x;
                }
            } else {
                anchor = selStart = selectionRange.x;
                cursorPos = selEnd = selectionRange.y;
            }
        } else {
            if (e.shiftDown()) {
                cursorPos = iFor(x);
                if (cursorPos > anchor) {
                    selStart = anchor;
                    selEnd = cursorPos;
                } else {
                    selStart = cursorPos;
                    selEnd = anchor;
                }
            } else {
                anchor = cursorPos = selStart = selEnd = iFor(x);
            }
        }

        repaintClientArea();

        startMouseDragTimer();
        return true;
    }


    void startCursorTimer()
    {
        startTimer(DRAW_CURSOR, cursorInterval);
    }

    void startMouseDragTimer()
    {
        startTimer(DRAG_MOUSE, mouseDragInterval);
    }

    void startTimer(int newTask, int length)
    {
        CpTimer.getDefaultTimer().register(this, length, newTask);
    }

    void stopTimers()
    {
        CpTimer.getDefaultTimer().unregister(this, DRAW_CURSOR);
        CpTimer.getDefaultTimer().unregister(this, DRAG_MOUSE);
        if (cursorOn) {
            cursorOn = false;
            repaintClientArea();
        }
    }

    public long timerEvent(int eventType)
    {
        switch (eventType) {
        case DRAW_CURSOR:
            cursorOn = !cursorOn;
            repaintClientArea();
            return cursorInterval;
        case DRAG_MOUSE:
            Point mouseLocation = mouseLocation();
            doMouseDrag(mouseLocation.x, mouseLocation.y);
            return mouseDragInterval;
        }
        return 0;
    }


    public boolean gotFocus()
    {
        startCursorTimer();
        showingSelection = true;
        hasChangedSinceGotFocus = false;

        if (selectAllOnGettingFocus) {
            selectAll();
            absorbMouseClick = true;
        }
        return false;
    }

    public boolean lostFocus()
    {
        stopTimers();
        if (hideSelectionOnFocusLoss) {
            showingSelection = false;
            cursorOn = false;
            repaintClientArea();
        }
        notifyIfChanged();
        return false;
    }


    void notifyIfChanged()
    {
        if (hasChangedSinceGotFocus) {
            notifyObservers(OBJECT_CHANGED, getText());
        }
    }

    public boolean keyUp(Event e, int key)
    {
        startCursorTimer();
        return true;
    }

    public boolean keyDown(Event e, int key)
    {
        boolean changingText = false;

        stopTimers();
        enableDamageRepair(false);

        switch (key) {
        case Event.RIGHT:
        case Event.LEFT:
            if (key == Event.RIGHT) {
                if (cursorPos < textModel.length) {
                    cursorPos++;
                }
            } else {
                if (cursorPos > 0) {
                    cursorPos--;
                }
            }
            if (e.shiftDown()) {
                if (anchor <= cursorPos) {
                    selStart = anchor;
                    selEnd = cursorPos;
                } else {
                    selStart = cursorPos;
                    selEnd = anchor;
                }
            } else {
                selStart = selEnd = anchor = cursorPos;
            }
            cursorOn = true;
            repaintClientArea();
            break;
        // IGNORE THESE TWO
        case Event.UP:
        case Event.DOWN:
            break;
        case Event.HOME:
            cursorPos = 0;
            if (e.shiftDown()) {
                selStart = cursorPos;
                selEnd = anchor;
            } else {
                selStart = selEnd = anchor = cursorPos;
            }
            cursorOn = true;
            repaintClientArea();
            break;
        case Event.END:
            cursorPos = textModel.length;
            if (e.shiftDown()) {
                selStart = anchor;
                selEnd = cursorPos;
            } else {
                selStart = selEnd = anchor = cursorPos;
            }
            cursorOn = true;
            repaintClientArea();
            break;
        case 8: // Backspace
            if (!clearSelection()) {
                if (cursorPos > 0) {
                    cursorPos--;
                    textModel.deleteChars(cursorPos, cursorPos);
                    repaintClientArea();
                    changingText = true;
                }
            }
            sizeToFitIfNecessary();
            scrollIfNecessary();
            break;
        case 9: // Tab
            clearSelection();
            textModel.insertString(tabSpaces, cursorPos);
            cursorPos += tabSpaces.length();
            changingText = true;
            break;
        case 10: // Enter
            notifyObservers(TEXT_ACCEPTED);
            notifyIfChanged();
            break;
        case 127:   // Delete
            if (!clearSelection()) {
                if (cursorPos < textModel.length) {
                    textModel.deleteChars(cursorPos, cursorPos);
                    repaintClientArea();
                    changingText = true;
                }
            }
            sizeToFitIfNecessary();
            scrollIfNecessary();
            break;
        case 24: // Ctrl-X
            cut();
            sizeToFitIfNecessary();
            scrollIfNecessary();
            break;
        case 22: // Ctrl-V
            paste();
            sizeToFitIfNecessary();
            scrollIfNecessary();
            break;
        case 3: // Ctrl-C
            copy();
            break;
        default:
            if (outputConverter == null || outputConverter.isValidCharacter((char)key)) {
                clearSelection();
                textModel.insertChar((char)key, cursorPos);
                cursorPos++;
                selEnd = selStart = anchor = cursorPos;
                damage();
                changingText = true;
            } else {
                beep();
            }
            sizeToFitIfNecessary();
            scrollIfNecessary();
            break;
        }
        keepCursorVisible();
        enableDamageRepair(true);
        repairDamage();

        if (changingText) {
            changingText();
        }


        return true;
    }

    void changingText()
    {
        hasChanged = true;
        hasChangedSinceGotFocus = true;
        notifyObservers(OBJECT_CHANGING);
    }

    boolean clearSelection()
    {
        if (selStart != selEnd) {
            textModel.deleteChars(selStart, selEnd - 1);
            cursorPos = anchor = selEnd = selStart;
            changingText();
            return true;
        } else {
            return false;
        }
    }



    public void paint(Graphics g, Rectangle clipRect)
    {
        int x = 0;
        int xStart = 0;
        int xEnd = 0;
        FontMetrics fontMetrics;
        int fontHeight, fontTop, fontBaseline;
        fontMetrics = g.getFontMetrics();
        fontHeight = fontMetrics.getHeight();
        Rectangle clientRect = getClientRect();
        int maxAscent = fontMetrics.getMaxAscent();

        switch (verticalAlignment) {
        case ALIGN_TOP:
            fontTop = clientRect.y;
            break;
        case ALIGN_CENTER:
            fontTop = clientRect.y + (clientRect.height - fontHeight) / 2;
            break;
        case ALIGN_BOTTOM:
            fontTop = clientRect.y + clientRect.height - fontHeight;
            break;
        default:
            throw new IllegalArgumentException("Invalid vertical alignment");
        }

        fontBaseline = fontTop + maxAscent;

        g.setColor(getBackground());
        g.fillRect(0, 0, size().width, size().height);
        g.setColor(getForeground());

        if (textModel.length != 0) {

            int firstFullyVisibleIndex = getFirstFullyVisibleIndex(fontMetrics);

            // This is used to avoid the last character being drawn if it's partially clipped.
            // Painful to implement, but much prettier.
            int lastFullyVisibleIndex = getLastFullyVisibleIndex(fontMetrics);

            xStart = xFor(firstFullyVisibleIndex);

    		if (selStart == selEnd || !showingSelection) {
    		    g.drawChars(textModel.chars, firstFullyVisibleIndex, lastFullyVisibleIndex - firstFullyVisibleIndex + 1, xStart, fontBaseline);
    		} else {
    		    if (selStart > firstFullyVisibleIndex) {
    		        int numChars = Math.min(selStart, lastFullyVisibleIndex + 1) - firstFullyVisibleIndex;
    		        g.drawChars(textModel.chars, firstFullyVisibleIndex, numChars, xStart, fontBaseline);
    		        xStart += fontMetrics.charsWidth(textModel.chars, firstFullyVisibleIndex, numChars);
    		    }
    		    int firstSelChar = Math.max(selStart, firstFullyVisibleIndex);
    		    int boundedSelEnd = Math.min(selEnd, lastFullyVisibleIndex + 1);
    		    xEnd = xStart + fontMetrics.charsWidth(textModel.chars, firstSelChar, boundedSelEnd - firstSelChar);
    		    g.setColor(selectionBackColor);
    		    g.fillRect(xStart, fontTop, xEnd - xStart, fontHeight);
    		    g.setColor(selectionForeColor);
    		    g.drawChars(textModel.chars, firstSelChar, boundedSelEnd - firstSelChar, xStart, fontBaseline);
    		    g.setColor(getForeground());
    		    int boundedEnd = Math.min(textModel.length, lastFullyVisibleIndex + 1);
    		    if (selEnd < boundedEnd) {
    		        g.drawChars(textModel.chars, selEnd, boundedEnd - selEnd, xEnd, fontBaseline);
    		    }
            }
        }
        if (cursorOn) {
            x = xFor(cursorPos);
            g.drawLine(x, fontTop, x, fontTop + fontHeight);
        }
    }


    synchronized void scrollHome()
    {
        scrollIndexOffset = 0;
    }

    int getFirstFullyVisibleIndex(FontMetrics fontMetrics)
    {
        if (horizontalAlignment == ALIGN_LEFT) {
            return scrollIndexOffset;
        } else {
            int textWidth = 0;
            int entryFieldWidth = getClientRect().width;

            for (int i=textModel.length - scrollIndexOffset - 1; i >= 0; i--) {
                textWidth += fontMetrics.charWidth(textModel.chars[i]);
                if (textWidth > entryFieldWidth) {
                    return i + 1;
                }
            }
            return 0;
        }
    }

    int getLastFullyVisibleIndex(FontMetrics fontMetrics)
    {
        if (horizontalAlignment == ALIGN_LEFT) {
            int textWidth = 0;
            int entryFieldWidth = getClientRect().width;

            for (int i=scrollIndexOffset; i < textModel.length; i++) {
                textWidth += fontMetrics.charWidth(textModel.chars[i]);
                if (textWidth > entryFieldWidth) {
                    return i - 1;
                }
            }
            return textModel.length - 1;
        } else {
            return textModel.length - scrollIndexOffset - 1;
        }
    }

    int iFor(int x)
    {
        int currentCharWidth, textWidth;
        FontMetrics fontMetrics = getFontMetrics();
        Insets insets = insets();
        if (horizontalAlignment == ALIGN_LEFT) {
            textWidth = 0;
            for (int i = scrollIndexOffset; i < textModel.length; i++) {
                currentCharWidth = fontMetrics.charWidth(textModel.chars[i]);
                if (textWidth + currentCharWidth/2 > x - insets.left) {
                    return i;
                }
                textWidth += currentCharWidth;
            }
            return textModel.length;
        } else {
            int xFromRight = size().width - x - insets.right;
            textWidth = 0;

            for (int i= textModel.length - scrollIndexOffset - 1; i >= 0; i--) {
                currentCharWidth = fontMetrics.charWidth(textModel.chars[i]);
                if (xFromRight < textWidth + (currentCharWidth / 2)) {
                    return i + 1;
                }
                textWidth += currentCharWidth;
            }
            return 0;
        }
    }


    int xFor(int i)
    {
        Insets insets = insets();

        if (horizontalAlignment == ALIGN_LEFT) {
            int offset = insets.left + getTextLeftX();
            if (i == 0) {
                return offset;
            }
            return offset + getFontMetrics().charsWidth(textModel.chars, 0, i);
        } else {
            FontMetrics fontMetrics = getFontMetrics();

            int leftX = getClientRect().width - fontMetrics.charsWidth(textModel.chars, 0, textModel.length - scrollIndexOffset);
            return leftX + fontMetrics.charsWidth(textModel.chars, 0, i);
        }
    }


    int getTextLeftX()
    {
        if (scrollIndexOffset == 0) {
            return 0;
        }

        return -getFontMetrics().charsWidth(textModel.chars, 0, scrollIndexOffset);
    }


    void scrollIfNecessary()
    {
        FontMetrics fontMetrics = getFontMetrics();
        int currentlyVisibleWidth, nextCharWidth;
        Rectangle rect = getClientRect();


        // if we are scrolled beyond the size of the text,
        // make sure as much text as possible is visible.

        // First, watch out for the case where they remove more text than we were scrolled (e.g. select all and delete)
        scrollIndexOffset = Math.min(textModel.length, scrollIndexOffset);
        currentlyVisibleWidth = fontMetrics.charsWidth(textModel.chars, scrollIndexOffset, textModel.length - scrollIndexOffset);

        while (scrollIndexOffset > 0 &&
            currentlyVisibleWidth + (nextCharWidth = fontMetrics.charWidth(textModel.chars[scrollIndexOffset - 1])) < rect.width) {
            scrollIndexOffset--;
            currentlyVisibleWidth += nextCharWidth;
        }
    }

    void keepCursorVisible()
    {
        FontMetrics fontMetrics = getFontMetrics();
        Rectangle rect = getClientRect();
        int left = rect.x;
        int right = rect.x + rect.width;

        if (horizontalAlignment == ALIGN_LEFT) {
            while (xFor(cursorPos) > right) {
                scrollIndexOffset++;
            }
            while (xFor(cursorPos) < left) {
                scrollIndexOffset--;
            }
        } else {
            while (xFor(cursorPos) > right) {
                scrollIndexOffset--;
            }
            while (xFor(cursorPos) < left) {
                scrollIndexOffset++;
            }
        }
    }

    public void setSizeToFit(boolean sizeToFit)
    {
        this.sizeToFit = sizeToFit;
    }

    public boolean getSizeToFit()
    {
        return sizeToFit;
    }

    void sizeToFitIfNecessary()
    {
        if (sizeToFit) {
            Dimension newSize = preferredSize();
            Rectangle bounds = bounds();

            switch (horizontalAlignment) {
            case ALIGN_LEFT:
                // Make sure it fits in its parent's window.
                newSize.width = Math.min(newSize.width, getParent().size().width - bounds.x);
                resize(newSize);
                break;
            case ALIGN_RIGHT:
                newSize.width = Math.min(newSize.width, bounds.x + bounds.width - 20);
                reshape(bounds.x + bounds.width - newSize.width, bounds.y, newSize.width, newSize.height);
                break;
            case ALIGN_CENTER:
            // Not yet implemented...
                break;
            }
        }
    }

    public boolean usesFocus()
    {
        return true;
    }

    public boolean wantsLeftAndRightArrows()
    {
        return true;
    }

    public boolean wantsCr()
    {
        return wantsCr;
    }

    public void setWantsCr(boolean wantsCr)
    {
        this.wantsCr = wantsCr;
    }

    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;
    }



}

