/**
 * @author Stanislav Lapitsky
 * @version 1.0
 */

import java.awt.*;
import javax.swing.*;
import javax.swing.text.*;

public class Hyphenation extends StyledEditorKit {

    HyphenationViewFactory factory = new HyphenationViewFactory();
    public static final char[] BREAK_CHARS = {' ', '\n', '\t'};

    public static FontMetrics fm = null;

    public static boolean isBreakChar(char ch) {
        for (int i = 0; i < BREAK_CHARS.length; i++) {
            if (BREAK_CHARS[i] == ch) {
                return true;
            }
        }
        return false;
    }

    /**
     * Collateral method to create application GUI
     * @return JFrame
     */
    public JFrame init() {
        JFrame frame = new JFrame("Hyphenation");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        final JEditorPane editor = new JEditorPane();
        editor.setEditorKit(this);
        JScrollPane scroll = new JScrollPane(editor);
        frame.getContentPane().add(scroll);
        frame.setSize(200, 100);
        frame.setLocationRelativeTo(null);

        return frame;
    }

    public static int[] getHyphens(String word) {
        int[] res = new int[word.length() / 3];
        for (int i = 0; i < res.length; i++) {
            res[i] = i * 3;
        }
        return res;
    }

    public static void main(String[] args) throws Exception {
        new Hyphenation().init().setVisible(true);
    }

    /**
     * gets kit view factory.
     * @return ViewFactory
     */
    public ViewFactory getViewFactory() {
        return factory;
    }

    /**
     * The view factory class creates custom views for pagination
     * root view (SectionView class) and paragraph (PageableParagraphView class)
     *
     * @author Stanislav Lapitsky
     * @version 1.0
     */
    class HyphenationViewFactory implements ViewFactory {
        /**
         * Creates view for specified element.
         * @param elem Element parent element
         * @return View created view instance.
         */
        public View create(Element elem) {
            String kind = elem.getName();
            if (kind != null) {
                if (kind.equals(AbstractDocument.ContentElementName)) {
                    return new HyphenatedLabelView(elem);
                }
                else if (kind.equals(AbstractDocument.ParagraphElementName)) {
                    return new HyphenatedParagraphView(elem);
                }
                else if (kind.equals(AbstractDocument.SectionElementName)) {
                    return new BoxView(elem, View.Y_AXIS);
                }
                else if (kind.equals(StyleConstants.ComponentElementName)) {
                    return new ComponentView(elem);
                }
                else if (kind.equals(StyleConstants.IconElementName)) {
                    return new IconView(elem);
                }
            }
            // default to text display
            return new LabelView(elem);
        }
    }

    class HyphenatedParagraphView extends ParagraphView {
        protected int[] breakOffsets = new int[0];
        protected int size = 0;
        public HyphenatedParagraphView(Element elem) {
            super(elem);
        }

        public void preferenceChanged(View child, boolean width, boolean height) {
            super.preferenceChanged(child, width, height);
            calculateBreakOffsets();
        }

        protected void calculateBreakOffsets() {
            int start = getStartOffset();
            int end = getEndOffset();
            int len = end - start;
            size = 0;
            breakOffsets = new int[len / 5];
            try {
                String text = getDocument().getText(start, end - start);

                int index = text.indexOf(' ');
                int lastIndex = 0;
                while (index > -1) {
                    String word = text.substring(lastIndex, index);
                    addHyphenOffsets(word, lastIndex);
                    addBreakOffset(index + 1, true);
                    lastIndex = index + 1;
                    index = text.indexOf(' ', lastIndex);
                }
                if (lastIndex >= 0) {
                    String word = text.substring(lastIndex);
                    addHyphenOffsets(word, lastIndex);
                }
            }
            catch (BadLocationException ex) {
                //do nothing
            }
        }

        protected void addHyphenOffsets(String word, int startOffset) {
            if (word.length() == 0) {
                return;
            }
            int len = word.length();
            StringBuffer subWord = new StringBuffer();
            int start = 0;
            while (isBreakChar(word.charAt(start))) {
                start++;
                if (start >= len) {
                    return;
                }
            }
            char c;
            int startWord = start;
            for (int j = start; j < len; j++) {
                c = word.charAt(j);
                if (isBreakChar(word.charAt(j))) {
                    int offs[] = getHyphens(subWord.toString());
                    int cnt = offs.length;
                    for (int i = 0; i < cnt; i++) {
                        addBreakOffset(startOffset + startWord + offs[i], false);
                    }
                    subWord.delete(0, subWord.length());
                    startWord = j + 1;
                }
                else {
                    subWord.append(c);
                }
            }
            if (subWord.length() != 0) {
                int offs[] = getHyphens(subWord.toString());
                int cnt = offs.length;
                for (int i = 0; i < cnt; i++) {
                    addBreakOffset(startOffset + startWord + offs[i], false);
                }
            }
        }

        private void addBreakOffset(int offs, boolean isSpace) {
            if (breakOffsets.length == size) {
                int[] tmpOffset = breakOffsets;
                breakOffsets = new int[Math.max(1, size * 2)];
                System.arraycopy(tmpOffset, 0, breakOffsets, 0, tmpOffset.length);
            }
            breakOffsets[size] = offs;
            size++;
        }

    }

    class HyphenatedLabelView extends LabelView {
        public HyphenatedLabelView(Element elem) {
            super(elem);
        }

        protected int getBreakSpot(int p0, int p1) {
            HyphenatedParagraphView pView = null;
            if (getParent() != null && getParent().getParent() != null
                && getParent().getParent() instanceof HyphenatedParagraphView) {
                pView = (HyphenatedParagraphView) getParent().getParent();
            }
            if (pView != null) {
                int[] breakOffsets = pView.breakOffsets;
                int size = pView.size;
                int start = pView.getStartOffset();
                for (int i = size - 1; i >= 0; i--) {
                    if (start + breakOffsets[i] <= p1 && start + breakOffsets[i] > p0) {

                        return start + breakOffsets[i];
                    }
                }
            }
            return -1;
        }

        public int getBreakWeight(int axis, float pos, float len) {
            int start = getStartOffset();
            int length = getEndOffset() - start;
            length = getGlyphPainter().getBoundedPosition(this, start, pos, len) - start;

            if (getBreakSpot(start, start + length) != -1) {
                return ExcellentBreakWeight;
            }
            int res = super.getBreakWeight(axis, pos, Math.max(1.0F, len - getHyphenWidth()));
            if (res == BadBreakWeight) {
                res = GoodBreakWeight;
            }
            return res;
        }

        public View breakView(int axis, int p0, float pos, float len) {
            if (axis == View.X_AXIS) {
                checkPainter();
                float l = len - getHyphenWidth();
                if (l < 0) {
                    l = 0.0F;
                }
                int p1 = getGlyphPainter().getBoundedPosition(this, p0, pos, l);
                int breakSpot = getBreakSpot(p0, p1);

                if (breakSpot != -1) {
                    p1 = breakSpot;
                }
                // else, no break in the region, return a fragment of the
                // bounded region.
                if (p0 == getStartOffset() && p1 == getEndOffset()) {
                    return this;
                }
                return createFragment(p0, p1);
            }
            return this;
        }

        public float getHyphenWidth() {
            if (fm == null) {
                fm = Toolkit.getDefaultToolkit().getFontMetrics(getFont());
            }
            return fm.stringWidth("-");
        }

        protected boolean isShowHyphen() {
            int end = getEndOffset();
            try {
                if (getBreakSpot(end - 1, end) == end && !" ".equals(getDocument().getText(end - 1, 1))) {
                    return true;
                }
            }
            catch (BadLocationException ex) {
            }
            return false;
        }

        public float getPreferredSpan(int axis) {
            float span = super.getPreferredSpan(axis);
            if (axis == View.X_AXIS && isShowHyphen()) {
                span += getHyphenWidth();
            }
            return span;
        }

        public void paint(Graphics g, Shape a) {
            super.paint(g, a);
            if (isShowHyphen()) {
                Rectangle alloc = a instanceof Rectangle ? (Rectangle) a : a.getBounds();
                int last = (int) (getPreferredSpan(View.X_AXIS) - getHyphenWidth());
                Rectangle clip = new Rectangle(alloc.x + last, alloc.y, (int) getHyphenWidth(), alloc.height);
                Shape oldClip = g.getClip();
                g.setClip(clip);
                g.setFont(getFont());
                int charHeight = 0;
                if (getContainer() != null) {
                    charHeight = getContainer().getFontMetrics(getFont()).getHeight();
                    g.drawString("-", alloc.x + last, alloc.y + fm.getMaxAscent() + Math.round( (alloc.height - charHeight) / 2));
                }
                else {
                    g.drawString("-", alloc.x + last, alloc.y + alloc.height - fm.getMaxDescent());
                }
                g.setClip(oldClip);
            }
        }
    }
}