/*
 * Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package sun.swing.text;

import java.awt.ComponentOrientation;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Component;
import java.awt.Container;
import java.awt.font.FontRenderContext;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.awt.print.PrinterException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicReference;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.TitledBorder;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import javax.swing.text.Document;
import javax.swing.text.EditorKit;
import javax.swing.text.AbstractDocument;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.HTML;

import sun.font.FontDesignMetrics;

import sun.swing.text.html.FrameEditorPaneTag;

/**
 * An implementation of {@code Printable} to print {@code JTextComponent} with
 * the header and footer.
 *
 * <h1>
 * WARNING: this class is to be used in
 * javax.swing.text.JTextComponent only.
 * </h1>
 *
 * <p>
 * The implementation creates a new {@code JTextComponent} ({@code printShell})
 * to print the content using the {@code Document}, {@code EditorKit} and
 * rendering-affecting properties from the original {@code JTextComponent}.
 *
 * <p>
 * {@code printShell} is laid out on the first {@code print} invocation.
 *
 * <p>
 * This class can be used on any thread. Part of the implementation is executed
 * on the EDT though.
 *
 * @author Igor Kushnirskiy
 *
 * @since 1.6
 */
public class TextComponentPrintable implements CountingPrintable {


    private static final int LIST_SIZE = 1000;

    private boolean isLayouted = false;

    /*
     * The text component to print.
     */
    private final JTextComponent textComponentToPrint;

    /*
     * The FontRenderContext to layout and print with
     */
    private final AtomicReference<FontRenderContext> frc =
        new AtomicReference<FontRenderContext>(null);

    /**
     * Special text component used to print to the printer.
     */
    private final JTextComponent printShell;

    private final MessageFormat headerFormat;
    private final MessageFormat footerFormat;

    private static final float HEADER_FONT_SIZE = 18.0f;
    private static final float FOOTER_FONT_SIZE = 12.0f;

    private final Font headerFont;
    private final Font footerFont;

    /**
     * stores metrics for the unhandled rows. The only metrics we need are
     * yStart and yEnd when row is handled by updatePagesMetrics it is removed
     * from the list. Thus the head of the list is the fist row to handle.
     *
     * sorted
     */
    private final List<IntegerSegment> rowsMetrics;

    /**
     * thread-safe list for storing pages metrics. The only metrics we need are
     * yStart and yEnd.
     * It has to be thread-safe since metrics are calculated on
     * the printing thread and accessed on the EDT thread.
     *
     * sorted
     */
    private final List<IntegerSegment> pagesMetrics;

    /**
     * Returns {@code TextComponentPrintable} to print {@code textComponent}.
     *
     * @param textComponent {@code JTextComponent} to print
     * @param headerFormat the page header, or {@code null} for none
     * @param footerFormat the page footer, or {@code null} for none
     * @return {@code TextComponentPrintable} to print {@code textComponent}
     */
    public static Printable getPrintable(final JTextComponent textComponent,
            final MessageFormat headerFormat,
            final MessageFormat footerFormat) {

        if (textComponent instanceof JEditorPane
                && isFrameSetDocument(textComponent.getDocument())) {
            //for document with frames we create one printable per
            //frame and merge them with the CompoundPrintable.
            List<JEditorPane> frames = getFrames((JEditorPane) textComponent);
            List<CountingPrintable> printables =
                new ArrayList<CountingPrintable>();
            for (JEditorPane frame : frames) {
                printables.add((CountingPrintable)
                               getPrintable(frame, headerFormat, footerFormat));
            }
            return new CompoundPrintable(printables);
        } else {
            return new TextComponentPrintable(textComponent,
               headerFormat, footerFormat);
        }
    }

    /**
     * Checks whether the document has frames. Only HTMLDocument might
     * have frames.
     *
     * @param document the {@code Document} to check
     * @return {@code true} if the {@code document} has frames
     */
    private static boolean isFrameSetDocument(final Document document) {
        boolean ret = false;
        if (document instanceof HTMLDocument) {
            HTMLDocument htmlDocument = (HTMLDocument)document;
            if (htmlDocument.getIterator(HTML.Tag.FRAME).isValid()) {
                ret = true;
            }
        }
        return ret;
    }


    /**
     * Returns frames under the {@code editor}.
     * The frames are created if necessary.
     *
     * @param editor the {@JEditorPane} to find the frames for
     * @return list of all frames
     */
    private static List<JEditorPane> getFrames(final JEditorPane editor) {
        List<JEditorPane> list = new ArrayList<JEditorPane>();
        getFrames(editor, list);
        if (list.size() == 0) {
            //the frames have not been created yet.
            //let's trigger the frames creation.
            createFrames(editor);
            getFrames(editor, list);
        }
        return list;
    }

    /**
     * Adds all {@code JEditorPanes} under {@code container} tagged by {@code
     * FrameEditorPaneTag} to the {@code list}. It adds only top
     * level {@code JEditorPanes}.  For instance if there is a frame
     * inside the frame it will return the top frame only.
     *
     * @param c the container to find all frames under
     * @param list {@code List} to append the results too
     */
    private static void getFrames(final Container container, List<JEditorPane> list) {
        for (Component c : container.getComponents()) {
            if (c instanceof FrameEditorPaneTag
                && c instanceof JEditorPane ) { //it should be always JEditorPane
                list.add((JEditorPane) c);
            } else {
                if (c instanceof Container) {
                    getFrames((Container) c, list);
                }
            }
        }
    }

    /**
     * Triggers the frames creation for {@code JEditorPane}
     *
     * @param editor the {@code JEditorPane} to create frames for
     */
    private static void createFrames(final JEditorPane editor) {
        Runnable doCreateFrames =
            new Runnable() {
                public void run() {
                    final int WIDTH = 500;
                    final int HEIGHT = 500;
                    CellRendererPane rendererPane = new CellRendererPane();
                    rendererPane.add(editor);
                    //the values do not matter
                    //we only need to get frames created
                    rendererPane.setSize(WIDTH, HEIGHT);
                };
            };
        if (SwingUtilities.isEventDispatchThread()) {
            doCreateFrames.run();
        } else {
            try {
                SwingUtilities.invokeAndWait(doCreateFrames);
            } catch (Exception e) {
                if (e instanceof RuntimeException) {
                    throw (RuntimeException) e;
                } else {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    /**
     * Constructs  {@code TextComponentPrintable} to print {@code JTextComponent}
     * {@code textComponent} with {@code headerFormat} and {@code footerFormat}.
     *
     * @param textComponent {@code JTextComponent} to print
     * @param headerFormat the page header or {@code null} for none
     * @param footerFormat the page footer or {@code null} for none
     */
    private TextComponentPrintable(JTextComponent textComponent,
            MessageFormat headerFormat,
            MessageFormat footerFormat) {
        this.textComponentToPrint = textComponent;
        this.headerFormat = headerFormat;
        this.footerFormat = footerFormat;
        headerFont = textComponent.getFont().deriveFont(Font.BOLD,
            HEADER_FONT_SIZE);
        footerFont = textComponent.getFont().deriveFont(Font.PLAIN,
            FOOTER_FONT_SIZE);
        this.pagesMetrics =
            Collections.synchronizedList(new ArrayList<IntegerSegment>());
        this.rowsMetrics = new ArrayList<IntegerSegment>(LIST_SIZE);
        this.printShell = createPrintShell(textComponent);
    }


    /**
     * creates a printShell.
     * It creates closest text component to {@code textComponent}
     * which uses {@code frc} from the {@code TextComponentPrintable}
     * for the {@code getFontMetrics} method.
     *
     * @param textComponent {@code JTextComponent} to create a
     *        printShell for
     * @return the print shell
     */
    private JTextComponent createPrintShell(final JTextComponent textComponent) {
        if (SwingUtilities.isEventDispatchThread()) {
            return createPrintShellOnEDT(textComponent);
        } else {
            FutureTask<JTextComponent> futureCreateShell =
                new FutureTask<JTextComponent>(
                    new Callable<JTextComponent>() {
                        public JTextComponent call() throws Exception {
                            return createPrintShellOnEDT(textComponent);
                        }
                    });
            SwingUtilities.invokeLater(futureCreateShell);
            try {
                return futureCreateShell.get();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } catch (ExecutionException e) {
                Throwable cause = e.getCause();
                if (cause instanceof Error) {
                    throw (Error) cause;
                }
                if (cause instanceof RuntimeException) {
                    throw (RuntimeException) cause;
                }
                throw new AssertionError(cause);
            }
        }
    }
    private JTextComponent createPrintShellOnEDT(final JTextComponent textComponent) {
        assert SwingUtilities.isEventDispatchThread();

        JTextComponent ret = null;
        if (textComponent instanceof JPasswordField) {
            ret =
                new JPasswordField() {
                    {
                        setEchoChar(((JPasswordField) textComponent).getEchoChar());
                        setHorizontalAlignment(
                            ((JTextField) textComponent).getHorizontalAlignment());
                    }
                    @Override
                    public FontMetrics getFontMetrics(Font font) {
                        return (frc.get() == null)
                            ? super.getFontMetrics(font)
                            : FontDesignMetrics.getMetrics(font, frc.get());
                    }
                };
        } else if (textComponent instanceof JTextField) {
            ret =
                new JTextField() {
                    {
                        setHorizontalAlignment(
                            ((JTextField) textComponent).getHorizontalAlignment());
                    }
                    @Override
                    public FontMetrics getFontMetrics(Font font) {
                        return (frc.get() == null)
                            ? super.getFontMetrics(font)
                            : FontDesignMetrics.getMetrics(font, frc.get());
                    }
                };
        } else if (textComponent instanceof JTextArea) {
            ret =
                new JTextArea() {
                    {
                        JTextArea textArea = (JTextArea) textComponent;
                        setLineWrap(textArea.getLineWrap());
                        setWrapStyleWord(textArea.getWrapStyleWord());
                        setTabSize(textArea.getTabSize());
                    }
                    @Override
                    public FontMetrics getFontMetrics(Font font) {
                        return (frc.get() == null)
                            ? super.getFontMetrics(font)
                            : FontDesignMetrics.getMetrics(font, frc.get());
                    }
                };
        } else if (textComponent instanceof JTextPane) {
            ret =
                new JTextPane() {
                    @Override
                    public FontMetrics getFontMetrics(Font font) {
                        return (frc.get() == null)
                            ? super.getFontMetrics(font)
                            : FontDesignMetrics.getMetrics(font, frc.get());
                    }
                    @Override
                    public EditorKit getEditorKit() {
                        if (getDocument() == textComponent.getDocument()) {
                            return ((JTextPane) textComponent).getEditorKit();
                        } else {
                            return super.getEditorKit();
                        }
                    }
                };
        } else if (textComponent instanceof JEditorPane) {
            ret =
                new JEditorPane() {
                    @Override
                    public FontMetrics getFontMetrics(Font font) {
                        return (frc.get() == null)
                            ? super.getFontMetrics(font)
                            : FontDesignMetrics.getMetrics(font, frc.get());
                    }
                    @Override
                    public EditorKit getEditorKit() {
                        if (getDocument() == textComponent.getDocument()) {
                            return ((JEditorPane) textComponent).getEditorKit();
                        } else {
                            return super.getEditorKit();
                        }
                    }
                };
        }
        //want to occupy the whole width and height by text
        ret.setBorder(null);

        //set properties from the component to print
        ret.setOpaque(textComponent.isOpaque());
        ret.setEditable(textComponent.isEditable());
        ret.setEnabled(textComponent.isEnabled());
        ret.setFont(textComponent.getFont());
        ret.setBackground(textComponent.getBackground());
        ret.setForeground(textComponent.getForeground());
        ret.setComponentOrientation(
            textComponent.getComponentOrientation());

        if (ret instanceof JEditorPane) {
            ret.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES,
                textComponent.getClientProperty(
                JEditorPane.HONOR_DISPLAY_PROPERTIES));
            ret.putClientProperty(JEditorPane.W3C_LENGTH_UNITS,
                textComponent.getClientProperty(JEditorPane.W3C_LENGTH_UNITS));
            ret.putClientProperty("charset",
                textComponent.getClientProperty("charset"));
        }
        ret.setDocument(textComponent.getDocument());
        return ret;
    }




    /**
     * Returns the number of pages in this printable.
     * <p>
     * This number is defined only after {@code print} returns NO_SUCH_PAGE.
     *
     * @return the number of pages.
     */
    public int getNumberOfPages() {
        return pagesMetrics.size();
    }

    /**
     * See Printable.print for the API description.
     *
     * There are two parts in the implementation.
     * First part (print) is to be called on the printing thread.
     * Second part (printOnEDT) is to be called on the EDT only.
     *
     * print triggers printOnEDT
     */
    public int print(final Graphics graphics,
            final PageFormat pf,
            final int pageIndex) throws PrinterException {
        if (!isLayouted) {
            if (graphics instanceof Graphics2D) {
                frc.set(((Graphics2D)graphics).getFontRenderContext());
            }
            layout((int)Math.floor(pf.getImageableWidth()));
            calculateRowsMetrics();
        }
        int ret;
        if (!SwingUtilities.isEventDispatchThread()) {
            Callable<Integer> doPrintOnEDT = new Callable<Integer>() {
                public Integer call() throws Exception {
                    return printOnEDT(graphics, pf, pageIndex);
                }
            };
            FutureTask<Integer> futurePrintOnEDT =
                new FutureTask<Integer>(doPrintOnEDT);
            SwingUtilities.invokeLater(futurePrintOnEDT);
            try {
                ret = futurePrintOnEDT.get();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } catch (ExecutionException e) {
                Throwable cause = e.getCause();
                if (cause instanceof PrinterException) {
                    throw (PrinterException)cause;
                } else if (cause instanceof RuntimeException) {
                    throw (RuntimeException) cause;
                } else if (cause instanceof Error) {
                    throw (Error) cause;
                } else {
                    throw new RuntimeException(cause);
                }
            }
        } else {
            ret = printOnEDT(graphics, pf, pageIndex);
        }
        return ret;
    }


    /**
     * The EDT part of the print method.
     *
     * This method is to be called on the EDT only. Layout should be done before
     * calling this method.
     */
    private int printOnEDT(final Graphics graphics,
            final PageFormat pf,
            final int pageIndex) throws PrinterException {
        assert SwingUtilities.isEventDispatchThread();
        Border border = BorderFactory.createEmptyBorder();
        //handle header and footer
        if (headerFormat != null || footerFormat != null) {
            //Printable page enumeration is 0 base. We need 1 based.
            Object[] formatArg = new Object[]{Integer.valueOf(pageIndex + 1)};
            if (headerFormat != null) {
                border = new TitledBorder(border,
                    headerFormat.format(formatArg),
                    TitledBorder.CENTER, TitledBorder.ABOVE_TOP,
                    headerFont, printShell.getForeground());
            }
            if (footerFormat != null) {
                border = new TitledBorder(border,
                    footerFormat.format(formatArg),
                    TitledBorder.CENTER, TitledBorder.BELOW_BOTTOM,
                    footerFont, printShell.getForeground());
            }
        }
        Insets borderInsets = border.getBorderInsets(printShell);
        updatePagesMetrics(pageIndex,
            (int)Math.floor(pf.getImageableHeight()) - borderInsets.top
                           - borderInsets.bottom);

        if (pagesMetrics.size() <= pageIndex) {
            return NO_SUCH_PAGE;
        }

        Graphics2D g2d = (Graphics2D)graphics.create();

        g2d.translate(pf.getImageableX(), pf.getImageableY());
        border.paintBorder(printShell, g2d, 0, 0,
            (int)Math.floor(pf.getImageableWidth()),
            (int)Math.floor(pf.getImageableHeight()));

        g2d.translate(0, borderInsets.top);
        //want to clip only vertically
        Rectangle clip = new Rectangle(0, 0,
            (int) pf.getWidth(),
            pagesMetrics.get(pageIndex).end
                - pagesMetrics.get(pageIndex).start + 1);

        g2d.clip(clip);
        int xStart = 0;
        if (ComponentOrientation.RIGHT_TO_LEFT ==
                printShell.getComponentOrientation()) {
            xStart = (int) pf.getImageableWidth() - printShell.getWidth();
        }
        g2d.translate(xStart, - pagesMetrics.get(pageIndex).start);
        printShell.print(g2d);

        g2d.dispose();

        return Printable.PAGE_EXISTS;
    }


    private boolean needReadLock = false;

    /**
     * Tries to release document's readlock
     *
     * Note: Not to be called on the EDT.
     */
    private void releaseReadLock() {
        assert ! SwingUtilities.isEventDispatchThread();
        Document document = textComponentToPrint.getDocument();
        if (document instanceof AbstractDocument) {
            try {
                ((AbstractDocument) document).readUnlock();
                needReadLock = true;
            } catch (Error ignore) {
                // readUnlock() might throw StateInvariantError
            }
        }
    }


    /**
     * Tries to acquire document's readLock if it was released
     * in releaseReadLock() method.
     *
     * Note: Not to be called on the EDT.
     */
    private void acquireReadLock() {
        assert ! SwingUtilities.isEventDispatchThread();
        if (needReadLock) {
            try {
                /*
                 * wait until all the EDT events are processed
                 * some of the document changes are asynchronous
                 * we need to make sure we get the lock after those changes
                 */
                SwingUtilities.invokeAndWait(
                    new Runnable() {
                        public void run() {
                        }
                    });
            } catch (InterruptedException ignore) {
            } catch (java.lang.reflect.InvocationTargetException ignore) {
            }
            Document document = textComponentToPrint.getDocument();
            ((AbstractDocument) document).readLock();
            needReadLock = false;
        }
    }

    /**
     * Prepares {@code printShell} for printing.
     *
     * Sets properties from the component to print.
     * Sets width and FontRenderContext.
     *
     * Triggers Views creation for the printShell.
     *
     * There are two parts in the implementation.
     * First part (layout) is to be called on the printing thread.
     * Second part (layoutOnEDT) is to be called on the EDT only.
     *
     * {@code layout} triggers {@code layoutOnEDT}.
     *
     * @param width width to layout the text for
     */
    private void layout(final int width) {
        if (!SwingUtilities.isEventDispatchThread()) {
            Callable<Object> doLayoutOnEDT = new Callable<Object>() {
                public Object call() throws Exception {
                    layoutOnEDT(width);
                    return null;
                }
            };
            FutureTask<Object> futureLayoutOnEDT = new FutureTask<Object>(
                doLayoutOnEDT);

            /*
             * We need to release document's readlock while printShell is
             * initializing
             */
            releaseReadLock();
            SwingUtilities.invokeLater(futureLayoutOnEDT);
            try {
                futureLayoutOnEDT.get();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } catch (ExecutionException e) {
                Throwable cause = e.getCause();
                if (cause instanceof RuntimeException) {
                    throw (RuntimeException) cause;
                } else if (cause instanceof Error) {
                    throw (Error) cause;
                } else {
                    throw new RuntimeException(cause);
                }
            } finally {
                acquireReadLock();
            }
        } else {
            layoutOnEDT(width);
        }

        isLayouted = true;
    }

    /**
     * The EDT part of layout method.
     *
     * This method is to be called on the EDT only.
     */
    private void layoutOnEDT(final int width) {
        assert SwingUtilities.isEventDispatchThread();
        //need to have big value but smaller than MAX_VALUE otherwise
        //printing goes south due to overflow somewhere
        final int HUGE_INTEGER = Integer.MAX_VALUE - 1000;

        CellRendererPane rendererPane = new CellRendererPane();

        //need to use JViewport since text is layouted to the viewPort width
        //otherwise it will be layouted to the maximum text width
        JViewport viewport = new JViewport();
        viewport.setBorder(null);
        Dimension size = new Dimension(width, HUGE_INTEGER);

        //JTextField is a special case
        //it layouts text in the middle by Y
        if (printShell instanceof JTextField) {
            size =
                new Dimension(size.width, printShell.getPreferredSize().height);
        }
        printShell.setSize(size);
        viewport.setComponentOrientation(printShell.getComponentOrientation());
        viewport.setSize(size);
        viewport.add(printShell);
        rendererPane.add(viewport);
    }

    /**
     * Calculates pageMetrics for the pages up to the {@code pageIndex} using
     * {@code rowsMetrics}.
     * Metrics are stored in the {@code pagesMetrics}.
     *
     * @param pageIndex the page to update the metrics for
     * @param pageHeight the page height
     */
    private void updatePagesMetrics(final int pageIndex, final int pageHeight) {
        while (pageIndex >= pagesMetrics.size() && !rowsMetrics.isEmpty()) {
            // add one page to the pageMetrics
            int lastPage = pagesMetrics.size() - 1;
            int pageStart = (lastPage >= 0)
               ? pagesMetrics.get(lastPage).end + 1
               : 0;
            int rowIndex;
            for (rowIndex = 0;
                   rowIndex < rowsMetrics.size()
                   && (rowsMetrics.get(rowIndex).end - pageStart + 1)
                     <= pageHeight;
                   rowIndex++) {
            }
            if (rowIndex == 0) {
                // can not fit a single row
                // need to split
                pagesMetrics.add(
                    new IntegerSegment(pageStart, pageStart + pageHeight - 1));
            } else {
                rowIndex--;
                pagesMetrics.add(new IntegerSegment(pageStart,
                    rowsMetrics.get(rowIndex).end));
                for (int i = 0; i <= rowIndex; i++) {
                    rowsMetrics.remove(0);
                }
            }
        }
    }

    /**
     * Calculates rowsMetrics for the document. The result is stored
     * in the {@code rowsMetrics}.
     *
     * Two steps process.
     * First step is to find yStart and yEnd for the every document position.
     * Second step is to merge all intersected segments ( [yStart, yEnd] ).
     */
    private void calculateRowsMetrics() {
        final int documentLength = printShell.getDocument().getLength();
        List<IntegerSegment> documentMetrics = new ArrayList<IntegerSegment>(LIST_SIZE);
        Rectangle rect;
        for (int i = 0, previousY = -1, previousHeight = -1; i < documentLength;
                 i++) {
            try {
                rect = printShell.modelToView(i);
                if (rect != null) {
                    int y = (int) rect.getY();
                    int height = (int) rect.getHeight();
                    if (height != 0
                            && (y != previousY || height != previousHeight)) {
                        /*
                         * we do not store the same value as previous. in our
                         * documents it is often for consequent positons to have
                         * the same modelToView y and height.
                         */
                        previousY = y;
                        previousHeight = height;
                        documentMetrics.add(new IntegerSegment(y, y + height - 1));
                    }
                }
            } catch (BadLocationException e) {
                assert false;
            }
        }
        /*
         * Merge all intersected segments.
         */
        Collections.sort(documentMetrics);
        int yStart = Integer.MIN_VALUE;
        int yEnd = Integer.MIN_VALUE;
        for (IntegerSegment segment : documentMetrics) {
            if (yEnd < segment.start) {
                if (yEnd != Integer.MIN_VALUE) {
                    rowsMetrics.add(new IntegerSegment(yStart, yEnd));
                }
                yStart = segment.start;
                yEnd = segment.end;
            } else {
                yEnd = segment.end;
            }
        }
        if (yEnd != Integer.MIN_VALUE) {
            rowsMetrics.add(new IntegerSegment(yStart, yEnd));
        }
    }

    /**
     *  Class to represent segment of integers.
     *  we do not call it Segment to avoid confusion with
     *  javax.swing.text.Segment
     */
    private static class IntegerSegment implements Comparable<IntegerSegment> {
        final int start;
        final int end;

        IntegerSegment(int start, int end) {
            this.start = start;
            this.end = end;
        }

        public int compareTo(IntegerSegment object) {
            int startsDelta = start - object.start;
            return (startsDelta != 0) ? startsDelta : end - object.end;
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof IntegerSegment) {
                return compareTo((IntegerSegment) obj) == 0;
            } else {
                return false;
            }
        }

        @Override
        public int hashCode() {
            // from the "Effective Java: Programming Language Guide"
            int result = 17;
            result = 37 * result + start;
            result = 37 * result + end;
            return result;
        }

        @Override
        public String toString() {
            return "IntegerSegment [" + start + ", " + end + "]";
        }
    }
}
