/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.graphics;

import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.SpannedString;
import android.text.TextUtils;

import java.awt.BasicStroke;
import java.awt.Font;
import java.awt.Toolkit;
import java.awt.font.FontRenderContext;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * A paint implementation overridden by the LayoutLib bridge.
 */
public class Paint extends _Original_Paint {

    private int mColor = 0xFFFFFFFF;
    private float mStrokeWidth = 1.f;
    private float mTextSize = 20;
    private float mScaleX = 1;
    private float mSkewX = 0;
    private Align mAlign = Align.LEFT;
    private Style mStyle = Style.FILL;
    private float mStrokeMiter = 4.0f;
    private Cap mCap = Cap.BUTT;
    private Join mJoin = Join.MITER;
    private int mFlags = 0;

    /**
     * Class associating a {@link Font} and it's {@link java.awt.FontMetrics}.
     */
    public static final class FontInfo {
        Font mFont;
        java.awt.FontMetrics mMetrics;
    }

    private List<FontInfo> mFonts;
    private final FontRenderContext mFontContext = new FontRenderContext(
            new AffineTransform(), true, true);

    public static final int ANTI_ALIAS_FLAG       = _Original_Paint.ANTI_ALIAS_FLAG;
    public static final int FILTER_BITMAP_FLAG    = _Original_Paint.FILTER_BITMAP_FLAG;
    public static final int DITHER_FLAG           = _Original_Paint.DITHER_FLAG;
    public static final int UNDERLINE_TEXT_FLAG   = _Original_Paint.UNDERLINE_TEXT_FLAG;
    public static final int STRIKE_THRU_TEXT_FLAG = _Original_Paint.STRIKE_THRU_TEXT_FLAG;
    public static final int FAKE_BOLD_TEXT_FLAG   = _Original_Paint.FAKE_BOLD_TEXT_FLAG;
    public static final int LINEAR_TEXT_FLAG      = _Original_Paint.LINEAR_TEXT_FLAG;
    public static final int SUBPIXEL_TEXT_FLAG    = _Original_Paint.SUBPIXEL_TEXT_FLAG;
    public static final int DEV_KERN_TEXT_FLAG    = _Original_Paint.DEV_KERN_TEXT_FLAG;

    public static class FontMetrics extends _Original_Paint.FontMetrics {
    }

    public static class FontMetricsInt extends _Original_Paint.FontMetricsInt {
    }

    /**
     * The Style specifies if the primitive being drawn is filled,
     * stroked, or both (in the same color). The default is FILL.
     */
    public enum Style {
        /**
         * Geometry and text drawn with this style will be filled, ignoring all
         * stroke-related settings in the paint.
         */
        FILL            (0),
        /**
         * Geometry and text drawn with this style will be stroked, respecting
         * the stroke-related fields on the paint.
         */
        STROKE          (1),
        /**
         * Geometry and text drawn with this style will be both filled and
         * stroked at the same time, respecting the stroke-related fields on
         * the paint.
         */
        FILL_AND_STROKE (2);

        Style(int nativeInt) {
            this.nativeInt = nativeInt;
        }
        final int nativeInt;
    }

    /**
     * The Cap specifies the treatment for the beginning and ending of
     * stroked lines and paths. The default is BUTT.
     */
    public enum Cap {
        /**
         * The stroke ends with the path, and does not project beyond it.
         */
        BUTT    (0),
        /**
         * The stroke projects out as a square, with the center at the end
         * of the path.
         */
        ROUND   (1),
        /**
         * The stroke projects out as a semicircle, with the center at the
         * end of the path.
         */
        SQUARE  (2);

        private Cap(int nativeInt) {
            this.nativeInt = nativeInt;
        }
        final int nativeInt;

        /** custom for layoutlib */
        public int getJavaCap() {
            switch (this) {
                case BUTT:
                    return BasicStroke.CAP_BUTT;
                case ROUND:
                    return BasicStroke.CAP_ROUND;
                default:
                case SQUARE:
                    return BasicStroke.CAP_SQUARE;
            }
        }
    }

    /**
     * The Join specifies the treatment where lines and curve segments
     * join on a stroked path. The default is MITER.
     */
    public enum Join {
        /**
         * The outer edges of a join meet at a sharp angle
         */
        MITER   (0),
        /**
         * The outer edges of a join meet in a circular arc.
         */
        ROUND   (1),
        /**
         * The outer edges of a join meet with a straight line
         */
        BEVEL   (2);

        private Join(int nativeInt) {
            this.nativeInt = nativeInt;
        }
        final int nativeInt;

        /** custom for layoutlib */
        public int getJavaJoin() {
            switch (this) {
                default:
                case MITER:
                    return BasicStroke.JOIN_MITER;
                case ROUND:
                    return BasicStroke.JOIN_ROUND;
                case BEVEL:
                    return BasicStroke.JOIN_BEVEL;
            }
        }
    }

    /**
     * Align specifies how drawText aligns its text relative to the
     * [x,y] coordinates. The default is LEFT.
     */
    public enum Align {
        /**
         * The text is drawn to the right of the x,y origin
         */
        LEFT    (0),
        /**
         * The text is drawn centered horizontally on the x,y origin
         */
        CENTER  (1),
        /**
         * The text is drawn to the left of the x,y origin
         */
        RIGHT   (2);

        private Align(int nativeInt) {
            this.nativeInt = nativeInt;
        }
        final int nativeInt;
    }

    public Paint() {
        this(0);
    }

    /*
     * Do not remove or com.android.layoutlib.bridge.TestClassReplacement fails.
     */
    @Override
    public void finalize() { }

    public Paint(int flags) {
        setFlags(flags | DEFAULT_PAINT_FLAGS);
        initFont();
    }

    public Paint(Paint paint) {
        set(paint);
        initFont();
    }

    @Override
    public void reset() {
        super.reset();
    }

    /**
     * Returns the list of {@link Font} objects. The first item is the main font, the rest
     * are fall backs for characters not present in the main font.
     */
    public List<FontInfo> getFonts() {
        return mFonts;
    }

    private void initFont() {
        mTypeface = Typeface.DEFAULT;
        updateFontObject();
    }

    /**
     * Update the {@link Font} object from the typeface, text size and scaling
     */
    @SuppressWarnings("deprecation")
    private void updateFontObject() {
        if (mTypeface != null) {
            // Get the fonts from the TypeFace object.
            List<Font> fonts = mTypeface.getFonts();

            // create new font objects as well as FontMetrics, based on the current text size
            // and skew info.
            ArrayList<FontInfo> infoList = new ArrayList<FontInfo>(fonts.size());
            for (Font font : fonts) {
                FontInfo info = new FontInfo();
                info.mFont = font.deriveFont(mTextSize);
                if (mScaleX != 1.0 || mSkewX != 0) {
                    // TODO: support skew
                    info.mFont = info.mFont.deriveFont(new AffineTransform(
                            mScaleX, mSkewX, 0, 0, 1, 0));
                }
                info.mMetrics = Toolkit.getDefaultToolkit().getFontMetrics(info.mFont);

                infoList.add(info);
            }

            mFonts = Collections.unmodifiableList(infoList);
        }
    }

    //----------------------------------------

    public void set(Paint src) {
        if (this != src) {
            mColor = src.mColor;
            mTextSize = src.mTextSize;
            mScaleX = src.mScaleX;
            mSkewX = src.mSkewX;
            mAlign = src.mAlign;
            mStyle = src.mStyle;
            mFlags = src.mFlags;

            super.set(src);
        }
    }

    @Override
    public void setCompatibilityScaling(float factor) {
        super.setCompatibilityScaling(factor);
    }

    @Override
    public int getFlags() {
        return mFlags;
    }

    @Override
    public void setFlags(int flags) {
        mFlags = flags;
    }

    @Override
    public boolean isAntiAlias() {
        return super.isAntiAlias();
    }

    @Override
    public boolean isDither() {
        return super.isDither();
    }

    @Override
    public boolean isLinearText() {
        return super.isLinearText();
    }

    @Override
    public boolean isStrikeThruText() {
        return super.isStrikeThruText();
    }

    @Override
    public boolean isUnderlineText() {
        return super.isUnderlineText();
    }

    @Override
    public boolean isFakeBoldText() {
        return super.isFakeBoldText();
    }

    @Override
    public boolean isSubpixelText() {
        return super.isSubpixelText();
    }

    @Override
    public boolean isFilterBitmap() {
        return super.isFilterBitmap();
    }

    /**
     * Return the font's recommended interline spacing, given the Paint's
     * settings for typeface, textSize, etc. If metrics is not null, return the
     * fontmetric values in it.
     *
     * @param metrics If this object is not null, its fields are filled with
     *                the appropriate values given the paint's text attributes.
     * @return the font's recommended interline spacing.
     */
    public float getFontMetrics(FontMetrics metrics) {
        if (mFonts.size() > 0) {
            java.awt.FontMetrics javaMetrics = mFonts.get(0).mMetrics;
            if (metrics != null) {
                // Android expects negative ascent so we invert the value from Java.
                metrics.top = - javaMetrics.getMaxAscent();
                metrics.ascent = - javaMetrics.getAscent();
                metrics.descent = javaMetrics.getDescent();
                metrics.bottom = javaMetrics.getMaxDescent();
                metrics.leading = javaMetrics.getLeading();
            }

            return javaMetrics.getHeight();
        }

        return 0;
    }

    public int getFontMetricsInt(FontMetricsInt metrics) {
        if (mFonts.size() > 0) {
            java.awt.FontMetrics javaMetrics = mFonts.get(0).mMetrics;
            if (metrics != null) {
                // Android expects negative ascent so we invert the value from Java.
                metrics.top = - javaMetrics.getMaxAscent();
                metrics.ascent = - javaMetrics.getAscent();
                metrics.descent = javaMetrics.getDescent();
                metrics.bottom = javaMetrics.getMaxDescent();
                metrics.leading = javaMetrics.getLeading();
            }

            return javaMetrics.getHeight();
        }

        return 0;
    }

    /**
     * Reimplemented to return Paint.FontMetrics instead of _Original_Paint.FontMetrics
     */
    public FontMetrics getFontMetrics() {
        FontMetrics fm = new FontMetrics();
        getFontMetrics(fm);
        return fm;
    }

    /**
     * Reimplemented to return Paint.FontMetricsInt instead of _Original_Paint.FontMetricsInt
     */
    public FontMetricsInt getFontMetricsInt() {
        FontMetricsInt fm = new FontMetricsInt();
        getFontMetricsInt(fm);
        return fm;
    }



    @Override
    public float getFontMetrics(_Original_Paint.FontMetrics metrics) {
        throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN");
    }

    @Override
    public int getFontMetricsInt(_Original_Paint.FontMetricsInt metrics) {
        throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN");
    }

    @Override
    public Typeface setTypeface(Typeface typeface) {
        if (typeface != null) {
            mTypeface = typeface;
        } else {
            mTypeface = Typeface.DEFAULT;
        }

        updateFontObject();

        return typeface;
    }

    @Override
    public Typeface getTypeface() {
        return super.getTypeface();
    }

    @Override
    public int getColor() {
        return mColor;
    }

    @Override
    public void setColor(int color) {
        mColor = color;
    }

    @Override
    public void setARGB(int a, int r, int g, int b) {
        super.setARGB(a, r, g, b);
    }

    @Override
    public void setAlpha(int alpha) {
        mColor = (alpha << 24) | (mColor & 0x00FFFFFF);
    }

    @Override
    public int getAlpha() {
        return mColor >>> 24;
    }

    /**
     * Set or clear the shader object.
     * <p />
     * Pass null to clear any previous shader.
     * As a convenience, the parameter passed is also returned.
     *
     * @param shader May be null. the new shader to be installed in the paint
     * @return       shader
     */
    @Override
    public Shader setShader(Shader shader) {
        return mShader = shader;
    }

    @Override
    public Shader getShader() {
        return super.getShader();
    }

    /**
     * Set or clear the paint's colorfilter, returning the parameter.
     *
     * @param filter May be null. The new filter to be installed in the paint
     * @return       filter
     */
    @Override
    public ColorFilter setColorFilter(ColorFilter filter) {
        mColorFilter = filter;
        return filter;
    }

    @Override
    public ColorFilter getColorFilter() {
        return super.getColorFilter();
    }

    /**
     * Set or clear the xfermode object.
     * <p />
     * Pass null to clear any previous xfermode.
     * As a convenience, the parameter passed is also returned.
     *
     * @param xfermode May be null. The xfermode to be installed in the paint
     * @return         xfermode
     */
    @Override
    public Xfermode setXfermode(Xfermode xfermode) {
        return mXfermode = xfermode;
    }

    @Override
    public Xfermode getXfermode() {
        return super.getXfermode();
    }

    @Override
    public Rasterizer setRasterizer(Rasterizer rasterizer) {
        mRasterizer = rasterizer;
        return rasterizer;
    }

    @Override
    public Rasterizer getRasterizer() {
        return super.getRasterizer();
    }

    @Override
    public void setShadowLayer(float radius, float dx, float dy, int color) {
        // TODO Auto-generated method stub
    }

    @Override
    public void clearShadowLayer() {
        super.clearShadowLayer();
    }

    public void setTextAlign(Align align) {
        mAlign = align;
    }

    @Override
    public void setTextAlign(android.graphics._Original_Paint.Align align) {
        throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN");
    }

    public Align getTextAlign() {
        return mAlign;
    }

    public void setStyle(Style style) {
        mStyle = style;
    }

    @Override
    public void setStyle(android.graphics._Original_Paint.Style style) {
        throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN");
    }

    public Style getStyle() {
        return mStyle;
    }

    @Override
    public void setDither(boolean dither) {
        mFlags |= dither ? DITHER_FLAG : ~DITHER_FLAG;
    }

    @Override
    public void setAntiAlias(boolean aa) {
        mFlags |= aa ? ANTI_ALIAS_FLAG : ~ANTI_ALIAS_FLAG;
    }

    @Override
    public void setFakeBoldText(boolean flag) {
        mFlags |= flag ? FAKE_BOLD_TEXT_FLAG : ~FAKE_BOLD_TEXT_FLAG;
    }

    @Override
    public void setLinearText(boolean flag) {
        mFlags |= flag ? LINEAR_TEXT_FLAG : ~LINEAR_TEXT_FLAG;
    }

    @Override
    public void setSubpixelText(boolean flag) {
        mFlags |= flag ? SUBPIXEL_TEXT_FLAG : ~SUBPIXEL_TEXT_FLAG;
    }

    @Override
    public void setUnderlineText(boolean flag) {
        mFlags |= flag ? UNDERLINE_TEXT_FLAG : ~UNDERLINE_TEXT_FLAG;
    }

    @Override
    public void setStrikeThruText(boolean flag) {
        mFlags |= flag ? STRIKE_THRU_TEXT_FLAG : ~STRIKE_THRU_TEXT_FLAG;
    }

    @Override
    public void setFilterBitmap(boolean flag) {
        mFlags |= flag ? FILTER_BITMAP_FLAG : ~FILTER_BITMAP_FLAG;
    }

    @Override
    public float getStrokeWidth() {
        return mStrokeWidth;
    }

    @Override
    public void setStrokeWidth(float width) {
        mStrokeWidth = width;
    }

    @Override
    public float getStrokeMiter() {
        return mStrokeMiter;
    }

    @Override
    public void setStrokeMiter(float miter) {
        mStrokeMiter = miter;
    }

    @Override
    public void setStrokeCap(android.graphics._Original_Paint.Cap cap) {
        throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN");
    }

    public void setStrokeCap(Cap cap) {
        mCap = cap;
    }

    public Cap getStrokeCap() {
        return mCap;
    }

    @Override
    public void setStrokeJoin(android.graphics._Original_Paint.Join join) {
        throw new UnsupportedOperationException("CALL TO PARENT FORBIDDEN");
    }

    public void setStrokeJoin(Join join) {
        mJoin = join;
    }

    public Join getStrokeJoin() {
        return mJoin;
    }

    @Override
    public boolean getFillPath(Path src, Path dst) {
        return super.getFillPath(src, dst);
    }

    @Override
    public PathEffect setPathEffect(PathEffect effect) {
        mPathEffect = effect;
        return effect;
    }

    @Override
    public PathEffect getPathEffect() {
        return super.getPathEffect();
    }

    @Override
    public MaskFilter setMaskFilter(MaskFilter maskfilter) {
        mMaskFilter = maskfilter;
        return maskfilter;
    }

    @Override
    public MaskFilter getMaskFilter() {
        return super.getMaskFilter();
    }

    /**
     * Return the paint's text size.
     *
     * @return the paint's text size.
     */
    @Override
    public float getTextSize() {
        return mTextSize;
    }

    /**
     * Set the paint's text size. This value must be > 0
     *
     * @param textSize set the paint's text size.
     */
    @Override
    public void setTextSize(float textSize) {
        mTextSize = textSize;

        updateFontObject();
    }

    /**
     * Return the paint's horizontal scale factor for text. The default value
     * is 1.0.
     *
     * @return the paint's scale factor in X for drawing/measuring text
     */
    @Override
    public float getTextScaleX() {
        return mScaleX;
    }

    /**
     * Set the paint's horizontal scale factor for text. The default value
     * is 1.0. Values > 1.0 will stretch the text wider. Values < 1.0 will
     * stretch the text narrower.
     *
     * @param scaleX set the paint's scale in X for drawing/measuring text.
     */
    @Override
    public void setTextScaleX(float scaleX) {
        mScaleX = scaleX;

        updateFontObject();
    }

    /**
     * Return the paint's horizontal skew factor for text. The default value
     * is 0.
     *
     * @return         the paint's skew factor in X for drawing text.
     */
    @Override
    public float getTextSkewX() {
        return mSkewX;
    }

    /**
     * Set the paint's horizontal skew factor for text. The default value
     * is 0. For approximating oblique text, use values around -0.25.
     *
     * @param skewX set the paint's skew factor in X for drawing text.
     */
    @Override
    public void setTextSkewX(float skewX) {
        mSkewX = skewX;

        updateFontObject();
    }

    @Override
    public float getFontSpacing() {
        return super.getFontSpacing();
    }

    /**
     * Return the distance above (negative) the baseline (ascent) based on the
     * current typeface and text size.
     *
     * @return the distance above (negative) the baseline (ascent) based on the
     *         current typeface and text size.
     */
    @Override
    public float ascent() {
        if (mFonts.size() > 0) {
            java.awt.FontMetrics javaMetrics = mFonts.get(0).mMetrics;
            // Android expects negative ascent so we invert the value from Java.
            return - javaMetrics.getAscent();
        }

        return 0;
    }

    /**
     * Return the distance below (positive) the baseline (descent) based on the
     * current typeface and text size.
     *
     * @return the distance below (positive) the baseline (descent) based on
     *         the current typeface and text size.
     */
    @Override
    public float descent() {
        if (mFonts.size() > 0) {
            java.awt.FontMetrics javaMetrics = mFonts.get(0).mMetrics;
            return javaMetrics.getDescent();
        }

        return 0;
    }

    /**
     * Return the width of the text.
     *
     * @param text  The text to measure
     * @param index The index of the first character to start measuring
     * @param count THe number of characters to measure, beginning with start
     * @return      The width of the text
     */
    @Override
    public float measureText(char[] text, int index, int count) {
        // WARNING: the logic in this method is similar to Canvas.drawText.
        // Any change to this method should be reflected in Canvas.drawText
        if (mFonts.size() > 0) {
            FontInfo mainFont = mFonts.get(0);
            int i = index;
            int lastIndex = index + count;
            float total = 0f;
            while (i < lastIndex) {
                // always start with the main font.
                int upTo = mainFont.mFont.canDisplayUpTo(text, i, lastIndex);
                if (upTo == -1) {
                    // shortcut to exit
                    return total + mainFont.mMetrics.charsWidth(text, i, lastIndex - i);
                } else if (upTo > 0) {
                    total += mainFont.mMetrics.charsWidth(text, i, upTo - i);
                    i = upTo;
                    // don't call continue at this point. Since it is certain the main font
                    // cannot display the font a index upTo (now ==i), we move on to the
                    // fallback fonts directly.
                }

                // no char supported, attempt to read the next char(s) with the
                // fallback font. In this case we only test the first character
                // and then go back to test with the main font.
                // Special test for 2-char characters.
                boolean foundFont = false;
                for (int f = 1 ; f < mFonts.size() ; f++) {
                    FontInfo fontInfo = mFonts.get(f);

                    // need to check that the font can display the character. We test
                    // differently if the char is a high surrogate.
                    int charCount = Character.isHighSurrogate(text[i]) ? 2 : 1;
                    upTo = fontInfo.mFont.canDisplayUpTo(text, i, i + charCount);
                    if (upTo == -1) {
                        total += fontInfo.mMetrics.charsWidth(text, i, charCount);
                        i += charCount;
                        foundFont = true;
                        break;

                    }
                }

                // in case no font can display the char, measure it with the main font.
                if (foundFont == false) {
                    int size = Character.isHighSurrogate(text[i]) ? 2 : 1;
                    total += mainFont.mMetrics.charsWidth(text, i, size);
                    i += size;
                }
            }
        }

        return 0;
    }

    /**
     * Return the width of the text.
     *
     * @param text  The text to measure
     * @param start The index of the first character to start measuring
     * @param end   1 beyond the index of the last character to measure
     * @return      The width of the text
     */
    @Override
    public float measureText(String text, int start, int end) {
        return measureText(text.toCharArray(), start, end - start);
    }

    /**
     * Return the width of the text.
     *
     * @param text  The text to measure
     * @return      The width of the text
     */
    @Override
    public float measureText(String text) {
        return measureText(text.toCharArray(), 0, text.length());
    }

    /*
     * re-implement to call SpannableStringBuilder.measureText with a Paint object
     * instead of an _Original_Paint
     */
    @Override
    public float measureText(CharSequence text, int start, int end) {
        if (text instanceof String) {
            return measureText((String)text, start, end);
        }
        if (text instanceof SpannedString ||
            text instanceof SpannableString) {
            return measureText(text.toString(), start, end);
        }
        if (text instanceof SpannableStringBuilder) {
            return ((SpannableStringBuilder)text).measureText(start, end, this);
        }

        char[] buf = TemporaryBuffer.obtain(end - start);
        TextUtils.getChars(text, start, end, buf, 0);
        float result = measureText(buf, 0, end - start);
        TemporaryBuffer.recycle(buf);
        return result;
    }

    /**
     * Measure the text, stopping early if the measured width exceeds maxWidth.
     * Return the number of chars that were measured, and if measuredWidth is
     * not null, return in it the actual width measured.
     *
     * @param text  The text to measure
     * @param index The offset into text to begin measuring at
     * @param count The number of maximum number of entries to measure. If count
     *              is negative, then the characters before index are measured
     *              in reverse order. This allows for measuring the end of
     *              string.
     * @param maxWidth The maximum width to accumulate.
     * @param measuredWidth Optional. If not null, returns the actual width
     *                     measured.
     * @return The number of chars that were measured. Will always be <=
     *         abs(count).
     */
    @Override
    public int breakText(char[] text, int index, int count,
                                float maxWidth, float[] measuredWidth) {
        int inc = count > 0 ? 1 : -1;

        int measureIndex = 0;
        float measureAcc = 0;
        for (int i = index ; i != index + count ; i += inc, measureIndex++) {
            int start, end;
            if (i < index) {
                start = i;
                end = index;
            } else {
                start = index;
                end = i;
            }

            // measure from start to end
            float res = measureText(text, start, end - start + 1);

            if (measuredWidth != null) {
                measuredWidth[measureIndex] = res;
            }

            measureAcc += res;
            if (res > maxWidth) {
                // we should not return this char index, but since it's 0-based and we need
                // to return a count, we simply return measureIndex;
                return measureIndex;
            }

        }

        return measureIndex;
    }

    /**
     * Measure the text, stopping early if the measured width exceeds maxWidth.
     * Return the number of chars that were measured, and if measuredWidth is
     * not null, return in it the actual width measured.
     *
     * @param text  The text to measure
     * @param measureForwards If true, measure forwards, starting at index.
     *                        Otherwise, measure backwards, starting with the
     *                        last character in the string.
     * @param maxWidth The maximum width to accumulate.
     * @param measuredWidth Optional. If not null, returns the actual width
     *                     measured.
     * @return The number of chars that were measured. Will always be <=
     *         abs(count).
     */
    @Override
    public int breakText(String text, boolean measureForwards,
                                float maxWidth, float[] measuredWidth) {
        return breakText(text,
                0 /* start */, text.length() /* end */,
                measureForwards, maxWidth, measuredWidth);
    }

    /**
     * Measure the text, stopping early if the measured width exceeds maxWidth.
     * Return the number of chars that were measured, and if measuredWidth is
     * not null, return in it the actual width measured.
     *
     * @param text  The text to measure
     * @param start The offset into text to begin measuring at
     * @param end   The end of the text slice to measure.
     * @param measureForwards If true, measure forwards, starting at start.
     *                        Otherwise, measure backwards, starting with end.
     * @param maxWidth The maximum width to accumulate.
     * @param measuredWidth Optional. If not null, returns the actual width
     *                     measured.
     * @return The number of chars that were measured. Will always be <=
     *         abs(end - start).
     */
    @Override
    public int breakText(CharSequence text, int start, int end, boolean measureForwards,
            float maxWidth, float[] measuredWidth) {
        char[] buf = new char[end - start];
        int result;

        TextUtils.getChars(text, start, end, buf, 0);

        if (measureForwards) {
            result = breakText(buf, 0, end - start, maxWidth, measuredWidth);
        } else {
            result = breakText(buf, 0, -(end - start), maxWidth, measuredWidth);
        }

        return result;
    }

    /**
     * Return the advance widths for the characters in the string.
     *
     * @param text     The text to measure
     * @param index    The index of the first char to to measure
     * @param count    The number of chars starting with index to measure
     * @param widths   array to receive the advance widths of the characters.
     *                 Must be at least a large as count.
     * @return         the actual number of widths returned.
     */
    @Override
    public int getTextWidths(char[] text, int index, int count,
                             float[] widths) {
        if (mFonts.size() > 0) {
            if ((index | count) < 0 || index + count > text.length
                    || count > widths.length) {
                throw new ArrayIndexOutOfBoundsException();
            }

            // FIXME: handle multi-char characters.
            // Need to figure out if the lengths of the width array takes into account
            // multi-char characters.
            for (int i = 0; i < count; i++) {
                char c = text[i + index];
                boolean found = false;
                for (FontInfo info : mFonts) {
                    if (info.mFont.canDisplay(c)) {
                        widths[i] = info.mMetrics.charWidth(c);
                        found = true;
                        break;
                    }
                }

                if (found == false) {
                    // we stop there.
                    return i;
                }
            }

            return count;
        }

        return 0;
    }

    /**
     * Return the advance widths for the characters in the string.
     *
     * @param text   The text to measure
     * @param start  The index of the first char to to measure
     * @param end    The end of the text slice to measure
     * @param widths array to receive the advance widths of the characters.
     *               Must be at least a large as the text.
     * @return       the number of unichars in the specified text.
     */
    @Override
    public int getTextWidths(String text, int start, int end, float[] widths) {
        if ((start | end | (end - start) | (text.length() - end)) < 0) {
            throw new IndexOutOfBoundsException();
        }
        if (end - start > widths.length) {
            throw new ArrayIndexOutOfBoundsException();
        }

        return getTextWidths(text.toCharArray(), start, end - start, widths);
    }

    /*
     * re-implement to call SpannableStringBuilder.getTextWidths with a Paint object
     * instead of an _Original_Paint
     */
    @Override
    public int getTextWidths(CharSequence text, int start, int end, float[] widths) {
        if (text instanceof String) {
            return getTextWidths((String)text, start, end, widths);
        }
        if (text instanceof SpannedString || text instanceof SpannableString) {
            return getTextWidths(text.toString(), start, end, widths);
        }
        if (text instanceof SpannableStringBuilder) {
            return ((SpannableStringBuilder)text).getTextWidths(start, end, widths, this);
        }

        char[] buf = TemporaryBuffer.obtain(end - start);
        TextUtils.getChars(text, start, end, buf, 0);
        int result = getTextWidths(buf, 0, end - start, widths);
        TemporaryBuffer.recycle(buf);
        return result;
    }

    @Override
    public int getTextWidths(String text, float[] widths) {
        return super.getTextWidths(text, widths);
    }

    /**
     * Return the path (outline) for the specified text.
     * Note: just like Canvas.drawText, this will respect the Align setting in
     * the paint.
     *
     * @param text     The text to retrieve the path from
     * @param index    The index of the first character in text
     * @param count    The number of characterss starting with index
     * @param x        The x coordinate of the text's origin
     * @param y        The y coordinate of the text's origin
     * @param path     The path to receive the data describing the text. Must
     *                 be allocated by the caller.
     */
    @Override
    public void getTextPath(char[] text, int index, int count,
                            float x, float y, Path path) {

        // TODO this is the ORIGINAL implementation. REPLACE AS NEEDED OR REMOVE

        if ((index | count) < 0 || index + count > text.length) {
            throw new ArrayIndexOutOfBoundsException();
        }

        // TODO native_getTextPath(mNativePaint, text, index, count, x, y, path.ni());

        throw new UnsupportedOperationException("IMPLEMENT AS NEEDED");
    }

    /**
     * Return the path (outline) for the specified text.
     * Note: just like Canvas.drawText, this will respect the Align setting
     * in the paint.
     *
     * @param text  The text to retrieve the path from
     * @param start The first character in the text
     * @param end   1 past the last charcter in the text
     * @param x     The x coordinate of the text's origin
     * @param y     The y coordinate of the text's origin
     * @param path  The path to receive the data describing the text. Must
     *              be allocated by the caller.
     */
    @Override
    public void getTextPath(String text, int start, int end,
                            float x, float y, Path path) {
        if ((start | end | (end - start) | (text.length() - end)) < 0) {
            throw new IndexOutOfBoundsException();
        }

        getTextPath(text.toCharArray(), start, end - start, x, y, path);
    }

    /**
     * Return in bounds (allocated by the caller) the smallest rectangle that
     * encloses all of the characters, with an implied origin at (0,0).
     *
     * @param text  String to measure and return its bounds
     * @param start Index of the first char in the string to measure
     * @param end   1 past the last char in the string measure
     * @param bounds Returns the unioned bounds of all the text. Must be
     *               allocated by the caller.
     */
    @Override
    public void getTextBounds(String text, int start, int end, Rect bounds) {
        if ((start | end | (end - start) | (text.length() - end)) < 0) {
            throw new IndexOutOfBoundsException();
        }
        if (bounds == null) {
            throw new NullPointerException("need bounds Rect");
        }

        getTextBounds(text.toCharArray(), start, end - start, bounds);
    }

    /**
     * Return in bounds (allocated by the caller) the smallest rectangle that
     * encloses all of the characters, with an implied origin at (0,0).
     *
     * @param text  Array of chars to measure and return their unioned bounds
     * @param index Index of the first char in the array to measure
     * @param count The number of chars, beginning at index, to measure
     * @param bounds Returns the unioned bounds of all the text. Must be
     *               allocated by the caller.
     */
    @Override
    public void getTextBounds(char[] text, int index, int count, Rect bounds) {
        // FIXME
        if (mFonts.size() > 0) {
            if ((index | count) < 0 || index + count > text.length) {
                throw new ArrayIndexOutOfBoundsException();
            }
            if (bounds == null) {
                throw new NullPointerException("need bounds Rect");
            }

            FontInfo mainInfo = mFonts.get(0);

            Rectangle2D rect = mainInfo.mFont.getStringBounds(text, index, index + count, mFontContext);
            bounds.set(0, 0, (int)rect.getWidth(), (int)rect.getHeight());
        }
    }

    public static void finalizer(int foo) {
        // pass
    }
}
