// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
/*
 *******************************************************************************
 * Copyright (C) 1996-2010, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 */
package com.ibm.icu.dev.demo.impl;
import java.awt.AWTEventMulticaster;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.TextEvent;
import java.awt.event.TextListener;
import java.text.BreakIterator;

// LIU: Changed from final to non-final
public class DumbTextComponent extends Canvas
  implements KeyListener, MouseListener, MouseMotionListener, FocusListener
{
    
    /**
     * For serialization
     */
    private static final long serialVersionUID = 8265547730738652151L;

//    private transient static final String copyright =
//      "Copyright \u00A9 1998, Mark Davis. All Rights Reserved.";
    private transient static boolean DEBUG = false;

    private String contents = "";
    private Selection selection = new Selection();
    private int activeStart = -1;
    private boolean editable = true;

    private transient Selection tempSelection = new Selection();
    private transient boolean focus;
    private transient BreakIterator lineBreaker = BreakIterator.getLineInstance();
    private transient BreakIterator wordBreaker = BreakIterator.getWordInstance();
    private transient BreakIterator charBreaker = BreakIterator.getCharacterInstance();
    private transient int lineAscent;
    private transient int lineHeight;
    private transient int lineLeading;
    private transient int lastHeight = 10;
    private transient int lastWidth = 50;
    private static final int MAX_LINES = 200; // LIU: Use symbolic name
    private transient int[] lineStarts = new int[MAX_LINES]; // LIU
    private transient int lineCount = 1;

    private transient boolean valid = false;
    private transient FontMetrics fm;
    private transient boolean redoLines = true;
    private transient boolean doubleClick = false;
    private transient TextListener textListener;
    private transient ActionListener selectionListener;
    private transient Image cacheImage;
    private transient Dimension mySize;
    private transient int xInset = 5;
    private transient int yInset = 5;
    private transient Point startPoint = new Point();
    private transient Point endPoint = new Point();
    private transient Point caretPoint = new Point();
    private transient Point activePoint = new Point();
    
    //private transient static String clipBoard;

    private static final char CR = '\015'; // LIU

    // ============================================

    public DumbTextComponent() {
        addMouseListener(this);
        addMouseMotionListener(this);
        addKeyListener(this);
        addFocusListener(this);
        setCursor(Cursor.getPredefinedCursor(Cursor.TEXT_CURSOR));

    }

// ================ Events ====================

    // public boolean isFocusTraversable() { return true; }

    public void addActionListener(ActionListener l) {
        selectionListener = AWTEventMulticaster.add(selectionListener, l);
    }

    public void removeActionListener(ActionListener l) {
        selectionListener = AWTEventMulticaster.remove(selectionListener, l);
    }

    public void addTextListener(TextListener l) {
        textListener = AWTEventMulticaster.add(textListener, l);
    }

    public void removeTextListener(TextListener l) {
        textListener = AWTEventMulticaster.remove(textListener, l);
    }

    private transient boolean pressed;

    public void mousePressed(MouseEvent e) {
        if (DEBUG) System.out.println("mousePressed");
        if (pressed) {
            select(e,false);
        } else {
            doubleClick = e.getClickCount() > 1;
            requestFocus();
            select(e, true);
            pressed = true;
        }
    }

    public void mouseDragged(MouseEvent e) {
        if (DEBUG) System.out.println("mouseDragged");
        select(e, false);
    }

    public void mouseReleased(MouseEvent e) {
        if (DEBUG) System.out.println("mouseReleased");
        pressed = false;
    }

    public void mouseEntered(MouseEvent e) {
        //if (pressed) select(e, false);
    }

    public void mouseExited(MouseEvent e){
        //if (pressed) select(e, false);
    }

    public void mouseClicked(MouseEvent e) {}
    public void mouseMoved(MouseEvent e) {}


    public void focusGained(FocusEvent e) {
        if (DEBUG) System.out.println("focusGained");
        focus = true;
        valid = false;
        repaint(16);
    }
    public void focusLost(FocusEvent e) {
        if (DEBUG) System.out.println("focusLost");
        focus = false;
        valid = false;
        repaint(16);
    }

    public void select(MouseEvent e, boolean first) {
        setKeyStart(-1);
        point2Offset(e.getPoint(), tempSelection);
        if (first) {
            if ((e.getModifiers() & InputEvent.SHIFT_MASK) == 0) {
                tempSelection.anchor = tempSelection.caret;
            }
        }
        // fix words
        if (doubleClick) {
            tempSelection.expand(wordBreaker);
        }
        select(tempSelection);
    }
    
    public void keyPressed(KeyEvent e) {
        int code = e.getKeyCode();
        if (DEBUG) System.out.println("keyPressed "
          + hex((char)code) + ", " + hex((char)e.getModifiers()));
        int start = selection.getStart();
        int end = selection.getEnd();
        boolean shift = (e.getModifiers() & InputEvent.SHIFT_MASK) != 0;
        boolean ctrl = (e.getModifiers() & InputEvent.CTRL_MASK) != 0;
                
        switch (code) {
        case KeyEvent.VK_Q:
            if (!ctrl || !editable) break;
            setKeyStart(-1);
            fixHex();
            break;
        case KeyEvent.VK_V:
            if (!ctrl) break;
            if (!editable) {
                this.getToolkit().beep();
            } else {
                paste();
            }
            break;
        case KeyEvent.VK_C:
            if (!ctrl) break;
            copy();
            break;
        case KeyEvent.VK_X:
            if (!ctrl) break;
            if (!editable) {
                this.getToolkit().beep();
            } else {
                copy();
                insertText("");
            }
            break;
        case KeyEvent.VK_A:
            if (!ctrl) break;
            setKeyStart(-1);
            select(Integer.MAX_VALUE, 0, false);
            break;
        case KeyEvent.VK_RIGHT:
            setKeyStart(-1);
            tempSelection.set(selection);
            tempSelection.nextBound(ctrl ? wordBreaker : charBreaker, +1, shift);
            select(tempSelection);
            break;
        case KeyEvent.VK_LEFT:
            setKeyStart(-1);
            tempSelection.set(selection);
            tempSelection.nextBound(ctrl ? wordBreaker : charBreaker, -1, shift);
            select(tempSelection);
            break;
        case KeyEvent.VK_UP: // LIU: Add support for up arrow
            setKeyStart(-1);
            tempSelection.set(selection);
            tempSelection.caret = lineDelta(tempSelection.caret, -1);
            if (!shift) {
                tempSelection.anchor = tempSelection.caret;
            }
            select(tempSelection);
            break;
        case KeyEvent.VK_DOWN: // LIU: Add support for down arrow
            setKeyStart(-1);
            tempSelection.set(selection);
            tempSelection.caret = lineDelta(tempSelection.caret, +1);
            if (!shift) {
                tempSelection.anchor = tempSelection.caret;
            }
            select(tempSelection);
            break;
        case KeyEvent.VK_DELETE: // LIU: Add delete key support
            if (!editable) break;
            setKeyStart(-1);
            if (contents.length() == 0) break;
            start = selection.getStart();
            end = selection.getEnd();
            if (start == end) {
                ++end;
                if (end > contents.length()) {
                    getToolkit().beep();
                    return;
                }
            }
            replaceRange("", start, end);
            break;            
        }
    }

    void copy() {
        Clipboard cb = this.getToolkit().getSystemClipboard();
        StringSelection ss = new StringSelection(
            contents.substring(selection.getStart(), selection.getEnd()));
        cb.setContents(ss, ss);
    }
    
    void paste () {
        Clipboard cb = this.getToolkit().getSystemClipboard();
        Transferable t = cb.getContents(this);
        if (t == null) {
            this.getToolkit().beep();
            return;
        }
        try {
            String temp = (String) t.getTransferData(DataFlavor.stringFlavor);
            insertText(temp);
        } catch (Exception e) {
            this.getToolkit().beep();
        }            
    }

    /**
     * LIU: Given an offset into contents, moves up or down by lines,
     * according to lineStarts[].
     * @param off the offset into contents
     * @param delta how many lines to move up (< 0) or down (> 0)
     * @return the new offset into contents
     */
    private int lineDelta(int off, int delta) {
        int line = findLine(off, false);
        int posInLine = off - lineStarts[line];
        // System.out.println("off=" + off + " at " + line + ":" + posInLine);
        line += delta;
        if (line < 0) {
            line = posInLine = 0;
        } else if (line >= lineCount) {
            return contents.length();
        }
        off = lineStarts[line] + posInLine;
        if (off >= lineStarts[line+1]) {
            off = lineStarts[line+1] - 1;
        }
        return off;
    }
      
    public void keyReleased(KeyEvent e) {
        int code = e.getKeyCode();
        if (DEBUG) System.out.println("keyReleased "
          + hex((char)code) + ", " + hex((char)e.getModifiers()));
    }

    public void keyTyped(KeyEvent e) {
        char ch = e.getKeyChar();
        if (DEBUG) System.out.println("keyTyped "
          + hex((char)ch) + ", " + hex((char)e.getModifiers()));
        if ((e.getModifiers() & InputEvent.CTRL_MASK) != 0) return;
        int start, end;
        switch (ch) {
        case KeyEvent.CHAR_UNDEFINED:
            break;
        case KeyEvent.VK_BACK_SPACE:
            //setKeyStart(-1);
            if (!editable) break;
            if (contents.length() == 0) break;
            start = selection.getStart();
            end = selection.getEnd();
            if (start == end) {
                --start;
                if (start < 0) {
                    getToolkit().beep(); // LIU: Add audio feedback of NOP
                    return;
                }
            }
            replaceRange("", start, end);
            break;        
        case KeyEvent.VK_DELETE:
            //setKeyStart(-1);
            if (!editable) break;
            if (contents.length() == 0) break;
            start = selection.getStart();
            end = selection.getEnd();
            if (start == end) {
                ++end;
                if (end > contents.length()) {
                    getToolkit().beep(); // LIU: Add audio feedback of NOP
                    return;
                }
            }
            replaceRange("", start, end);
            break;
        default:
            if (!editable) break;
            // LIU: Dispatch to subclass API
            handleKeyTyped(e);
            break;
        }
    }

    // LIU: Subclass API for handling of key typing
    protected void handleKeyTyped(KeyEvent e) {
        insertText(String.valueOf(e.getKeyChar()));
    }
    
    protected void setKeyStart(int keyStart) {
        if (activeStart != keyStart) {
            activeStart = keyStart;
            repaint(10);
        }
    }
    
    protected void validateKeyStart() {
        if (activeStart > selection.getStart()) {
            activeStart = selection.getStart();
            repaint(10);
        }
    }
    
    protected int getKeyStart() {
        return activeStart;
    }

// ===================== Control ======================

    public synchronized void setEditable(boolean b) {
        editable = b;
    }

    public boolean isEditable() {
        return editable;
    }

    public void select(Selection newSelection) {
        newSelection.pin(contents);
        if (!selection.equals(newSelection)) {
            selection.set(newSelection);
            if (selectionListener != null) {
                selectionListener.actionPerformed(
                  new ActionEvent(this, ActionEvent.ACTION_PERFORMED,
                    "Selection Changed", 0));
            }
            repaint(10);
            valid = false;
        }
    }

    public void select(int start, int end) {
        select(start, end, false);
    }

    public void select(int start, int end, boolean clickAfter) {
        tempSelection.set(start, end, clickAfter);
        select(tempSelection);
    }

    public int getSelectionStart() {
        return selection.getStart();
    }

    public int getSelectionEnd() {
        return selection.getEnd();
    }

    public void setBounds(int x, int y, int w, int h) {
        super.setBounds(x,y,w,h);
        redoLines = true;
    }

    public Dimension getPreferredSize() {
        return new Dimension(lastWidth,lastHeight);
    }

    public Dimension getMaximumSize() {
        return new Dimension(lastWidth,lastHeight);
    }

    public Dimension getMinimumSize() {
        return new Dimension(lastHeight,lastHeight);
    }

    public void setText(String text) {
        setText2(text);
        select(tempSelection.set(selection).pin(contents));
    }

    public void setText2(String text) {
        contents = text;
        charBreaker.setText(text);
        wordBreaker.setText(text);
        lineBreaker.setText(text);
        redoLines = true;
        if (textListener != null)
            textListener.textValueChanged(
              new TextEvent(this, TextEvent.TEXT_VALUE_CHANGED));
        repaint(16);
    }

    public void insertText(String text) {
        if (activeStart == -1) activeStart = selection.getStart();
        replaceRange(text, selection.getStart(), selection.getEnd());
    }

    public void replaceRange(String s, int start, int end) {
        setText2(contents.substring(0,start) + s
          + contents.substring(end));
        select(tempSelection.set(selection).
          fixAfterReplace(start, end, s.length()));
        validateKeyStart();
    }

    public String getText() {
        return contents;
    }

    public void setFont(Font font) {
        super.setFont(font);
        redoLines = true;
        repaint(16);
    }

    // ================== Graphics ======================

    public void update(Graphics g) {
        if (DEBUG) System.out.println("update");
        paint(g);
    }

    public void paint(Graphics g) {
        mySize = getSize();
        if (cacheImage == null
          || cacheImage.getHeight(this) != mySize.height
          || cacheImage.getWidth(this) != mySize.width) {
            cacheImage = createImage(mySize.width, mySize.height);
            valid = false;
        }
        if (!valid || redoLines) {
            if (DEBUG) System.out.println("painting");
            paint2(cacheImage.getGraphics());
            valid = true;
        }
        //getToolkit().sync();
        if (DEBUG) System.out.println("copying");
        g.drawImage(cacheImage,
          0, 0, mySize.width, mySize.height,
          0, 0, mySize.width, mySize.height,
          this);
    }

    public void paint2(Graphics g) {
        g.clearRect(0, 0, mySize.width, mySize.height);
        if (DEBUG) System.out.println("print");
        if (focus) g.setColor(Color.black);
        else g.setColor(Color.gray);
        g.drawRect(0,0,mySize.width-1,mySize.height-1);
        g.setClip(1,1,
          mySize.width-2,mySize.height-2);
        g.setColor(Color.black);
        g.setFont(getFont());
        fm = g.getFontMetrics();
        lineAscent = fm.getAscent();
        lineLeading = fm.getLeading();
        lineHeight = lineAscent + fm.getDescent() + lineLeading;
        int y = yInset + lineAscent;
        String lastSubstring = "";
        if (redoLines) fixLineStarts(mySize.width-xInset-xInset);
        for (int i = 0; i < lineCount; y += lineHeight, ++i) {
            // LIU: Don't display terminating ^M characters
            int lim = lineStarts[i+1];
            if (lim > 0 && contents.length() > 0 &&
                contents.charAt(lim-1) == CR) --lim;
            lastSubstring = contents.substring(lineStarts[i],lim);
            g.drawString(lastSubstring, xInset, y);
        }
        drawSelection(g, lastSubstring);
        lastHeight = y + yInset - lineHeight + yInset;
        lastWidth = mySize.width-xInset-xInset;
    }

    void paintRect(Graphics g, int x, int y, int w, int h) {
        if (focus) {
            g.fillRect(x, y, w, h);
        } else {
            g.drawRect(x, y, w-1, h-1);
        }
    }

    public void drawSelection(Graphics g, String lastSubstring) {
        g.setXORMode(Color.black);
        if (activeStart != -1) {
            offset2Point(activeStart, false, activePoint);
            g.setColor(Color.magenta);
            int line = activePoint.x - 1;
            g.fillRect(line, activePoint.y, 1, lineHeight);
        }
        if (selection.isCaret()) {
            offset2Point(selection.caret, selection.clickAfter, caretPoint);
        } else {
            if (focus) g.setColor(Color.blue);
            else g.setColor(Color.yellow);
            offset2Point(selection.getStart(), true, startPoint);
            offset2Point(selection.getEnd(), false, endPoint);
            if (selection.getStart() == selection.caret)
                caretPoint.setLocation(startPoint);
            else caretPoint.setLocation(endPoint);
            if (startPoint.y == endPoint.y) {
                paintRect(g, startPoint.x, startPoint.y,
                  Math.max(1,endPoint.x-startPoint.x), lineHeight);
            } else {
                paintRect(g, startPoint.x, startPoint.y,
                  (mySize.width-xInset)-startPoint.x, lineHeight);
                if (startPoint.y + lineHeight < endPoint.y)
                  paintRect(g, xInset, startPoint.y + lineHeight,
                  (mySize.width-xInset)-xInset, endPoint.y - startPoint.y - lineHeight);
                paintRect(g, xInset, endPoint.y, endPoint.x-xInset, lineHeight);
            }
        }
        if (focus || selection.isCaret()) {
            if (focus) g.setColor(Color.green);
            else g.setColor(Color.red);
            int line = caretPoint.x - (selection.clickAfter ? 0 : 1);
            g.fillRect(line, caretPoint.y, 1, lineHeight);
            int w = lineHeight/12 + 1;
            int braces = line - (selection.clickAfter ? -1 : w);
            g.fillRect(braces, caretPoint.y, w, 1);
            g.fillRect(braces, caretPoint.y + lineHeight - 1, w, 1);
        }
    }

    public Point offset2Point(int off, boolean start, Point p) {
        int line = findLine(off, start);
        int width = 0;
        try {
            width = fm.stringWidth(
              contents.substring(lineStarts[line], off));
        } catch (Exception e) {
            System.out.println(e);
        }
        p.x = width + xInset;
        if (p.x > mySize.width - xInset)
            p.x = mySize.width - xInset;
        p.y = lineHeight * line + yInset;
        return p;
    }

    private int findLine(int off, boolean start) {
        // if it is start, then go to the next line!
        if (start) ++off;
        for (int i = 1; i < lineCount; ++i) {
            // LIU: This was <= ; changed to < to make caret after
            // final CR in line appear at START of next line.
            if (off < lineStarts[i]) return i-1;
        }
        // LIU: Check for special case; after CR at end of the last line
        if (off == lineStarts[lineCount] &&
            off > 0 && contents.length() > 0 && contents.charAt(off-1) == CR) {
            return lineCount;
        }
        return lineCount-1;
    }

    // offsets on any line will go from start,true to end,false
    // excluding start,false and end,true
    public Selection point2Offset(Point p, Selection o) {
        if (p.y < yInset) {
            o.caret = 0;
            o.clickAfter = true;
            return o;
        }
        int line = (p.y - yInset)/lineHeight;
        if (line >= lineCount) {
            o.caret = contents.length();
            o.clickAfter = false;
            return o;
        }
        int target = p.x - xInset;
        if (target <= 0) {
            o.caret = lineStarts[line];
            o.clickAfter = true;
            return o;
        }
        int lowGuess = lineStarts[line];
        int lowWidth = 0;
        int highGuess = lineStarts[line+1];
        int highWidth = fm.stringWidth(contents.substring(lineStarts[line],highGuess));
        if (target >= highWidth) {
            o.caret = lineStarts[line+1];
            o.clickAfter = false;
            return o;
        }
        while (lowGuess < highGuess - 1) {
            int guess = (lowGuess + highGuess)/2;
            int width = fm.stringWidth(contents.substring(lineStarts[line],guess));
            if (width <= target) {
                lowGuess = guess;
                lowWidth = width;
                if (width == target) break;
            } else {
                highGuess = guess;
                highWidth = width;
            }
        }
        // at end, either lowWidth < target < width(low+1), or lowWidth = target
        int highBound = charBreaker.following(lowGuess);
        int lowBound = charBreaker.previous();
        // we are now at character boundaries
        if (lowBound != lowGuess)
            lowWidth = fm.stringWidth(contents.substring(lineStarts[line],lowBound));
        if (highBound != highGuess)
            highWidth = fm.stringWidth(contents.substring(lineStarts[line],highBound));
        // we now have the right widths
        if (target - lowWidth < highWidth - target) {
            o.caret = lowBound;
            o.clickAfter = true;
        } else {
            o.caret = highBound;
            o.clickAfter = false;
        }
        // we now have the closest!
        return o;
    }

    private void fixLineStarts(int width) {
        lineCount = 1;
        lineStarts[0] = 0;
        if (contents.length() == 0) {
            lineStarts[1] = 0;
            return;
        }
        int end = 0;
        // LIU: Add check for MAX_LINES
        for (int start = 0; start < contents.length() && lineCount < MAX_LINES;
             start = end) {
            end = nextLine(fm, start, width);
            lineStarts[lineCount++] = end;
            if (end == start) { // LIU: Assertion
                throw new RuntimeException("nextLine broken");
            }
        }
        --lineCount;
        redoLines = false;
    }

    // LIU: Enhanced to wrap long lines.  Bug with return of start fixed.
    public int nextLine(FontMetrics fMtr, int start, int width) {
        int len = contents.length();
        for (int i = start; i < len; ++i) {
            // check for line separator
            char ch = (contents.charAt(i));
            if (ch >= 0x000A && ch <= 0x000D || ch == 0x2028 || ch == 0x2029) {
                len = i + 1;
                if (ch == 0x000D && i+1 < len && contents.charAt(i+1) == 0x000A) // crlf
                    ++len; // grab extra char
                break;
            }
        }
        String subject = contents.substring(start,len);
        if (visibleWidth(fMtr, subject) <= width)
          return len;

        // LIU: Remainder of this method rewritten to accomodate lines
        // longer than the component width by first trying to break
        // into lines; then words; finally chars.
        int n = findFittingBreak(fMtr, subject, width, lineBreaker);
        if (n == 0) {
            n = findFittingBreak(fMtr, subject, width, wordBreaker);
        }
        if (n == 0) {
            n = findFittingBreak(fMtr, subject, width, charBreaker);
        }
        return n > 0 ? start + n : len;
    }

    /**
     * LIU: Finds the longest substring that fits a given width
     * composed of subunits returned by a BreakIterator.  If the smallest
     * subunit is too long, returns 0.
     * @param fMtr metrics to use
     * @param line the string to be fix into width
     * @param width line.substring(0, result) must be <= width
     * @param breaker the BreakIterator that will be used to find subunits
     * @return maximum characters, at boundaries returned by breaker,
     * that fit into width, or zero on failure
     */
    private int findFittingBreak(FontMetrics fMtr, String line, int width,
                                 BreakIterator breaker) {
        breaker.setText(line);
        int last = breaker.first();
        int end = breaker.next();
        while (end != BreakIterator.DONE &&
               visibleWidth(fMtr, line.substring(0, end)) <= width) {
            last = end;
            end = breaker.next();
        }
        return last;
    }

    public int visibleWidth(FontMetrics fMtr, String s) {
        int i;
        for (i = s.length()-1; i >= 0; --i) {
            char ch = s.charAt(i);
            if (!(ch == ' ' || ch >= 0x000A && ch <= 0x000D || ch == 0x2028 || ch == 0x2029))
                return fMtr.stringWidth(s.substring(0,i+1));
        }
        return 0;
    }

// =============== Utility ====================

    private void fixHex() {
        if (selection.getEnd() == 0) return;
        int store = 0;
        int places = 1;
        int count = 0;
        int min = Math.min(8,selection.getEnd());
        for (int i = 0; i < min; ++i) {
            char ch = contents.charAt(selection.getEnd()-1-i);
            int value = Character.getNumericValue(ch);
            if (value < 0 || value > 15) break;
            store += places * value;
            ++count;
            places *= 16;
        }
        String add = "";
        int bottom = store & 0xFFFF;
        if (store >= 0xD8000000 && store < 0xDC000000
          && bottom >= 0xDC00 && bottom < 0xE000) { // surrogates
            add = "" + (char)(store >> 16) + (char)bottom;
        } else if (store > 0xFFFF && store <= 0x10FFFF) {
            store -= 0x10000;
            add = "" + (char)(((store >> 10) & 0x3FF) + 0xD800)
              + (char)((store & 0x3FF) + 0xDC00);
              
        } else if (count >= 4) {
            count = 4;
            add = ""+(char)(store & 0xFFFF);
        } else {
            count = 1;
            char ch = contents.charAt(selection.getEnd()-1);
            add = hex(ch);
            if (ch >= 0xDC00 && ch <= 0xDFFF && selection.getEnd() > 1) {
                ch = contents.charAt(selection.getEnd()-2);
                if (ch >= 0xD800 && ch <= 0xDBFF) {
                    count = 2;
                    add = hex(ch) + add;
                }
            }
        }
        replaceRange(add, selection.getEnd()-count, selection.getEnd());
    }

    public static String hex(char ch) {
        String result = Integer.toString(ch,16).toUpperCase();
        result = "0000".substring(result.length(),4) + result;
        return result;
    }
}
