/*
 * Copyright (c) 1999, 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 javax.swing.text;

import java.text.*;
import java.awt.*;
import java.awt.font.*;
import java.awt.geom.Rectangle2D;

/**
 * A class to perform rendering of the glyphs.
 * This can be implemented to be stateless, or
 * to hold some information as a cache to
 * facilitate faster rendering and model/view
 * translation.  At a minimum, the GlyphPainter
 * allows a View implementation to perform its
 * duties independent of a particular version
 * of JVM and selection of capabilities (i.e.
 * shaping for i18n, etc).
 * <p>
 * This implementation is intended for operation
 * under the JDK.  It uses the
 * java.awt.font.TextLayout class to do i18n capable
 * rendering.
 *
 * @author  Timothy Prinzing
 * @see GlyphView
 */
class GlyphPainter2 extends GlyphView.GlyphPainter {

    public GlyphPainter2(TextLayout layout) {
        this.layout = layout;
    }

    /**
     * Create a painter to use for the given GlyphView.
     */
    public GlyphView.GlyphPainter getPainter(GlyphView v, int p0, int p1) {
        return null;
    }

    /**
     * Determine the span the glyphs given a start location
     * (for tab expansion).  This implementation assumes it
     * has no tabs (i.e. TextLayout doesn't deal with tab
     * expansion).
     */
    public float getSpan(GlyphView v, int p0, int p1,
                         TabExpander e, float x) {

        if ((p0 == v.getStartOffset()) && (p1 == v.getEndOffset())) {
            return layout.getAdvance();
        }
        int p = v.getStartOffset();
        int index0 = p0 - p;
        int index1 = p1 - p;

        TextHitInfo hit0 = TextHitInfo.afterOffset(index0);
        TextHitInfo hit1 = TextHitInfo.beforeOffset(index1);
        float[] locs = layout.getCaretInfo(hit0);
        float x0 = locs[0];
        locs = layout.getCaretInfo(hit1);
        float x1 = locs[0];
        return (x1 > x0) ? x1 - x0 : x0 - x1;
    }

    public float getHeight(GlyphView v) {
        return layout.getAscent() + layout.getDescent() + layout.getLeading();
    }

    /**
     * Fetch the ascent above the baseline for the glyphs
     * corresponding to the given range in the model.
     */
    public float getAscent(GlyphView v) {
        return layout.getAscent();
    }

    /**
     * Fetch the descent below the baseline for the glyphs
     * corresponding to the given range in the model.
     */
    public float getDescent(GlyphView v) {
        return layout.getDescent();
    }

    /**
     * Paint the glyphs for the given view.  This is implemented
     * to only render if the Graphics is of type Graphics2D which
     * is required by TextLayout (and this should be the case if
     * running on the JDK).
     */
    public void paint(GlyphView v, Graphics g, Shape a, int p0, int p1) {
        if (g instanceof Graphics2D) {
            Rectangle2D alloc = a.getBounds2D();
            Graphics2D g2d = (Graphics2D)g;
            float y = (float) alloc.getY() + layout.getAscent() + layout.getLeading();
            float x = (float) alloc.getX();
            if( p0 > v.getStartOffset() || p1 < v.getEndOffset() ) {
                try {
                    //TextLayout can't render only part of it's range, so if a
                    //partial range is required, add a clip region.
                    Shape s = v.modelToView(p0, Position.Bias.Forward,
                                            p1, Position.Bias.Backward, a);
                    Shape savedClip = g.getClip();
                    g2d.clip(s);
                    layout.draw(g2d, x, y);
                    g.setClip(savedClip);
                } catch (BadLocationException e) {}
            } else {
                layout.draw(g2d, x, y);
            }
        }
    }

    public Shape modelToView(GlyphView v, int pos, Position.Bias bias,
                             Shape a) throws BadLocationException {
        int offs = pos - v.getStartOffset();
        Rectangle2D alloc = a.getBounds2D();
        TextHitInfo hit = (bias == Position.Bias.Forward) ?
            TextHitInfo.afterOffset(offs) : TextHitInfo.beforeOffset(offs);
        float[] locs = layout.getCaretInfo(hit);

        // vertical at the baseline, should use slope and check if glyphs
        // are being rendered vertically.
        alloc.setRect(alloc.getX() + locs[0], alloc.getY(), 1, alloc.getHeight());
        return alloc;
    }

    /**
     * Provides a mapping from the view coordinate space to the logical
     * coordinate space of the model.
     *
     * @param v the view containing the view coordinates
     * @param x the X coordinate
     * @param y the Y coordinate
     * @param a the allocated region to render into
     * @param biasReturn either <code>Position.Bias.Forward</code>
     *  or <code>Position.Bias.Backward</code> is returned as the
     *  zero-th element of this array
     * @return the location within the model that best represents the
     *  given point of view
     * @see View#viewToModel
     */
    public int viewToModel(GlyphView v, float x, float y, Shape a,
                           Position.Bias[] biasReturn) {

        Rectangle2D alloc = (a instanceof Rectangle2D) ? (Rectangle2D)a : a.getBounds2D();
        //Move the y co-ord of the hit onto the baseline.  This is because TextLayout supports
        //italic carets and we do not.
        TextHitInfo hit = layout.hitTestChar(x - (float)alloc.getX(), 0);
        int pos = hit.getInsertionIndex();

        if (pos == v.getEndOffset()) {
            pos--;
        }

        biasReturn[0] = hit.isLeadingEdge() ? Position.Bias.Forward : Position.Bias.Backward;
        return pos + v.getStartOffset();
    }

    /**
     * Determines the model location that represents the
     * maximum advance that fits within the given span.
     * This could be used to break the given view.  The result
     * should be a location just shy of the given advance.  This
     * differs from viewToModel which returns the closest
     * position which might be proud of the maximum advance.
     *
     * @param v the view to find the model location to break at.
     * @param p0 the location in the model where the
     *  fragment should start it's representation >= 0.
     * @param pos the graphic location along the axis that the
     *  broken view would occupy >= 0.  This may be useful for
     *  things like tab calculations.
     * @param len specifies the distance into the view
     *  where a potential break is desired >= 0.
     * @return the maximum model location possible for a break.
     * @see View#breakView
     */
    public int getBoundedPosition(GlyphView v, int p0, float x, float len) {
        if( len < 0 )
            throw new IllegalArgumentException("Length must be >= 0.");
        // note: this only works because swing uses TextLayouts that are
        // only pure rtl or pure ltr
        TextHitInfo hit;
        if (layout.isLeftToRight()) {
            hit = layout.hitTestChar(len, 0);
        } else {
            hit = layout.hitTestChar(layout.getAdvance() - len, 0);
        }
        return v.getStartOffset() + hit.getCharIndex();
    }

    /**
         * Provides a way to determine the next visually represented model
         * location that one might place a caret.  Some views may not be
         * visible, they might not be in the same order found in the model, or
         * they just might not allow access to some of the locations in the
         * model.
         *
         * @param v the view to use
         * @param pos the position to convert >= 0
         * @param a the allocated region to render into
         * @param direction the direction from the current position that can
         *  be thought of as the arrow keys typically found on a keyboard.
         *  This may be SwingConstants.WEST, SwingConstants.EAST,
         *  SwingConstants.NORTH, or SwingConstants.SOUTH.
         * @return the location within the model that best represents the next
         *  location visual position.
         * @exception BadLocationException
         * @exception IllegalArgumentException for an invalid direction
         */
        public int getNextVisualPositionFrom(GlyphView v, int pos,
                                             Position.Bias b, Shape a,
                                             int direction,
                                             Position.Bias[] biasRet)
            throws BadLocationException {

            int startOffset = v.getStartOffset();
            int endOffset = v.getEndOffset();
            Segment text;
            AbstractDocument doc;
            boolean viewIsLeftToRight;
            TextHitInfo currentHit, nextHit;

            switch (direction) {
            case View.NORTH:
                break;
            case View.SOUTH:
                break;
            case View.EAST:
                doc = (AbstractDocument)v.getDocument();
                viewIsLeftToRight = doc.isLeftToRight(startOffset, endOffset);

                if(startOffset == doc.getLength()) {
                    if(pos == -1) {
                        biasRet[0] = Position.Bias.Forward;
                        return startOffset;
                    }
                    // End case for bidi text where newline is at beginning
                    // of line.
                    return -1;
                }
                if(pos == -1) {
                    // Entering view from the left.
                    if( viewIsLeftToRight ) {
                        biasRet[0] = Position.Bias.Forward;
                        return startOffset;
                    } else {
                        text = v.getText(endOffset - 1, endOffset);
                        char c = text.array[text.offset];
                        SegmentCache.releaseSharedSegment(text);
                        if(c == '\n') {
                            biasRet[0] = Position.Bias.Forward;
                            return endOffset-1;
                        }
                        biasRet[0] = Position.Bias.Backward;
                        return endOffset;
                    }
                }
                if( b==Position.Bias.Forward )
                    currentHit = TextHitInfo.afterOffset(pos-startOffset);
                else
                    currentHit = TextHitInfo.beforeOffset(pos-startOffset);
                nextHit = layout.getNextRightHit(currentHit);
                if( nextHit == null ) {
                    return -1;
                }
                if( viewIsLeftToRight != layout.isLeftToRight() ) {
                    // If the layout's base direction is different from
                    // this view's run direction, we need to use the weak
                    // carrat.
                    nextHit = layout.getVisualOtherHit(nextHit);
                }
                pos = nextHit.getInsertionIndex() + startOffset;

                if(pos == endOffset) {
                    // A move to the right from an internal position will
                    // only take us to the endOffset in a left to right run.
                    text = v.getText(endOffset - 1, endOffset);
                    char c = text.array[text.offset];
                    SegmentCache.releaseSharedSegment(text);
                    if(c == '\n') {
                        return -1;
                    }
                    biasRet[0] = Position.Bias.Backward;
                }
                else {
                    biasRet[0] = Position.Bias.Forward;
                }
                return pos;
            case View.WEST:
                doc = (AbstractDocument)v.getDocument();
                viewIsLeftToRight = doc.isLeftToRight(startOffset, endOffset);

                if(startOffset == doc.getLength()) {
                    if(pos == -1) {
                        biasRet[0] = Position.Bias.Forward;
                        return startOffset;
                    }
                    // End case for bidi text where newline is at beginning
                    // of line.
                    return -1;
                }
                if(pos == -1) {
                    // Entering view from the right
                    if( viewIsLeftToRight ) {
                        text = v.getText(endOffset - 1, endOffset);
                        char c = text.array[text.offset];
                        SegmentCache.releaseSharedSegment(text);
                        if ((c == '\n') || Character.isSpaceChar(c)) {
                            biasRet[0] = Position.Bias.Forward;
                            return endOffset - 1;
                        }
                        biasRet[0] = Position.Bias.Backward;
                        return endOffset;
                    } else {
                        biasRet[0] = Position.Bias.Forward;
                        return startOffset;
                   }
                }
                if( b==Position.Bias.Forward )
                    currentHit = TextHitInfo.afterOffset(pos-startOffset);
                else
                    currentHit = TextHitInfo.beforeOffset(pos-startOffset);
                nextHit = layout.getNextLeftHit(currentHit);
                if( nextHit == null ) {
                    return -1;
                }
                if( viewIsLeftToRight != layout.isLeftToRight() ) {
                    // If the layout's base direction is different from
                    // this view's run direction, we need to use the weak
                    // carrat.
                    nextHit = layout.getVisualOtherHit(nextHit);
                }
                pos = nextHit.getInsertionIndex() + startOffset;

                if(pos == endOffset) {
                    // A move to the left from an internal position will
                    // only take us to the endOffset in a right to left run.
                    text = v.getText(endOffset - 1, endOffset);
                    char c = text.array[text.offset];
                    SegmentCache.releaseSharedSegment(text);
                    if(c == '\n') {
                        return -1;
                    }
                    biasRet[0] = Position.Bias.Backward;
                }
                else {
                    biasRet[0] = Position.Bias.Forward;
                }
                return pos;
            default:
                throw new IllegalArgumentException("Bad direction: " + direction);
            }
            return pos;

        }
    // --- variables ---------------------------------------------

    TextLayout layout;

}
