/*
 * Copyright (c) 2009-2012 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.font;

import com.jme3.math.ColorRGBA;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;

/**
 * LetterQuad contains the position, color, and UV texture information for a character in text.
 * @author YongHoon
 */
class LetterQuad {
    private static final Rectangle UNBOUNDED = new Rectangle(0, 0, Float.MAX_VALUE, Float.MAX_VALUE);
    private static final float LINE_DIR = -1;

    private final BitmapFont font;
    private final char c;
    private final int index;
    private int style;

    private BitmapCharacter bitmapChar = null;
    private float x0 = Integer.MIN_VALUE;
    private float y0 = Integer.MIN_VALUE;
    private float width = Integer.MIN_VALUE;
    private float height = Integer.MIN_VALUE;
    private float xAdvance = 0;
    private float u0;
    private float v0;
    private float u1;
    private float v1;
    private float lineY;
    private boolean eol;

    private LetterQuad previous;
    private LetterQuad next;
    private int colorInt = 0xFFFFFFFF;

    final private boolean rightToLeft;
    private float alignX;
    private float alignY;
    private float sizeScale = 1;

    /**
     * create head / tail
     * @param font
     * @param rightToLeft
     */
    protected LetterQuad(BitmapFont font, boolean rightToLeft) {
        this.font = font;
        this.c = Character.MIN_VALUE;
        this.rightToLeft = rightToLeft;
        this.index = -1;
        setBitmapChar(null);
    }

    /**
     * create letter and append to previous LetterQuad
     *
     * @param c
     * @param prev previous character
     */
    protected LetterQuad(char c, LetterQuad prev) {
        this.font = prev.font;
        this.rightToLeft = prev.rightToLeft;
        this.c = c;
        this.index = prev.index+1;
        this.eol = isLineFeed();
        setBitmapChar(c);
        prev.insert(this);
    }

    LetterQuad addNextCharacter(char c) {
        LetterQuad n = new LetterQuad(c, this);
        return n;
    }

    BitmapCharacter getBitmapChar() {
        return bitmapChar;
    }

    char getChar() {
        return c;
    }

    int getIndex() {
        return index;
    }

    private Rectangle getBound(StringBlock block) {
        if (block.getTextBox() != null) {
            return block.getTextBox();
        }
        return UNBOUNDED;
    }

    LetterQuad getPrevious() {
        return previous;
    }

    LetterQuad getNext() {
        return next;
    }

    public float getU0() {
        return u0;
    }

    float getU1() {
        return u1;
    }

    float getV0() {
        return v0;
    }

    float getV1() {
        return v1;
    }

    boolean isRightToLeft() {
        return rightToLeft;
    }

    boolean isInvalid() {
        return x0 == Integer.MIN_VALUE;
    }

    boolean isInvalid(StringBlock block) {
        return isInvalid(block, 0);
    }

    boolean isInvalid(StringBlock block, float gap) {
        if (isHead() || isTail())
            return false;
        if (x0 == Integer.MIN_VALUE || y0 == Integer.MIN_VALUE) {
            return true;
        }
        Rectangle bound = block.getTextBox();
        if (bound == null) {
            return false;
        }
        if (isRightToLeft()) {
            return x0 <0 && x0<bound.x;
            // ToDo check for ellipsis, not sure if it is on both sides of a character and need to be deducted as well
        } else {
            return x0 > 0 && bound.x+bound.width-gap < getX1();
        }
    }

    void clip(StringBlock block) {
        Rectangle bound = block.getTextBox();
        if (bound == null)
            return;

        // Clip the right x position and texture coordinate
        // to the string block
        float x1 = Math.min(bound.x + bound.width, x0 + width);
        float newWidth = x1 - x0;
        if (isRightToLeft()) newWidth = x1; // only the available space to the left
        if (newWidth == width)
            return;

        float rescale = newWidth / width;
        u1 = u0 + (u1 - u0) * rescale;
        width = newWidth;
    }

    float getX0() {
        return x0;
    }

    float getX1() {
        return x0 + width;
    }
    float getNextX() {
        return rightToLeft ? x0 - xAdvance : x0 + xAdvance;
    }

    float getNextLine() {
        return lineY+LINE_DIR*font.getCharSet().getLineHeight() * sizeScale;
    }

    float getY0() {
        return y0;
    }

    float getY1() {
        return y0-height;
    }

    float getWidth() {
        return width;
    }

    float getHeight() {
        return height;
    }

    void insert(LetterQuad ins) {
        LetterQuad n = next;
        next = ins;
        ins.next = n;
        ins.previous = this;
        n.previous = ins;
    }

    void invalidate() {
        eol = isLineFeed();
        setBitmapChar(font.getCharSet().getCharacter(c, style));
    }

    boolean isTail() {
        return next == null;
    }

    boolean isHead() {
        return previous == null;
    }

    /**
     * @return next letter
     */
    LetterQuad remove() {
        this.previous.next = next;
        this.next.previous = previous;
        return next;
    }

    void setPrevious(LetterQuad before) {
        this.previous = before;
    }

    void setStyle(int style) {
        this.style = style;
        invalidate();
    }

    void setColor(ColorRGBA color) {
        this.colorInt = color.asIntRGBA();
        invalidate();
    }

    void setAlpha(float alpha) {
        int i = (int) (alpha * 255) & 0xFF;
        colorInt = (colorInt & 0xffffff00) | i;
        invalidate();
    }

    void setBitmapChar(char c) {
        BitmapCharacterSet charSet = font.getCharSet();
        BitmapCharacter bm = charSet.getCharacter(c, style);
        setBitmapChar(bm);
    }

    void setBitmapChar(BitmapCharacter bitmapChar) {
        x0 = Integer.MIN_VALUE;
        y0 = Integer.MIN_VALUE;
        width = Integer.MIN_VALUE;
        height = Integer.MIN_VALUE;
        alignX = 0;
        alignY = 0;

        BitmapCharacterSet charSet = font.getCharSet();
        this.bitmapChar = bitmapChar;
        if (bitmapChar != null) {
            u0 = (float) bitmapChar.getX() / charSet.getWidth();
            v0 = (float) bitmapChar.getY() / charSet.getHeight();
            u1 = u0 + (float) bitmapChar.getWidth() / charSet.getWidth();
            v1 = v0 + (float) bitmapChar.getHeight() / charSet.getHeight();
        } else {
            u0 = 0;
            v0 = 0;
            u1 = 0;
            v1 = 0;
        }
    }

    void setNext(LetterQuad next) {
        this.next = next;
    }

    void update(StringBlock block) {
        final float[] tabs = block.getTabPosition();
        final float tabWidth = block.getTabWidth();
        final Rectangle bound = getBound(block);
        sizeScale = block.getSize() / font.getCharSet().getRenderedSize();
        lineY = computeLineY(block);

        if (isHead()) {
            x0 = getBound(block).x;
            if (isRightToLeft() && getBound(block) != UNBOUNDED) {
                x0 += getBound(block).width;
            }
            y0 = lineY;
            width = 0;
            height = 0;
            xAdvance = 0;
        } else if (isTab()) {
            x0 = previous.getNextX();
            width = tabWidth;
            y0 = lineY;
            height = 0;
            if (tabs != null && x0 < tabs[tabs.length-1]) {
                for (int i = 0; i < tabs.length-1; i++) {
                    if (x0 > tabs[i] && x0 < tabs[i+1]) {
                        width = tabs[i+1] - x0;
                    }
                }
            }
            xAdvance = width;
        } else if (bitmapChar == null) {
            x0 = getPrevious().getX1();
            if (rightToLeft) x0 = getPrevious().getX0();
            y0 = lineY;
            width = 0;
            height = 0;
            xAdvance = 0;
        } else {
            float xOffset = bitmapChar.getXOffset() * sizeScale;
            float yOffset = bitmapChar.getYOffset() * sizeScale;
            xAdvance = bitmapChar.getXAdvance() * sizeScale;
            width = bitmapChar.getWidth() * sizeScale;
            height = bitmapChar.getHeight() * sizeScale;
            float incrScale = rightToLeft ? -1f : 1f;
            float kernAmount = 0f;

            if (previous.isHead() || previous.eol) {
                if (rightToLeft) {
                    // In RTL text we advance toward left by the letter xAdvance. (subtract xAdvance)
                    // Note, positive offset will move the letter quad toward right and negative offset
                    // will move it toward left.
                    if (previous.isHead()) {
                        x0 = previous.getNextX() - xAdvance - xOffset * incrScale;
                    } else if (previous.eol) {
                        // For bounded bitmap text the first letter of a line is always
                        // on the right end of the textbox and for unbounded bitmap text
                        // we start from the x=0 and advance toward left.
                        x0 = getBound(block).x + (getBound(block) != UNBOUNDED ? getBound(block).width : 0) - xAdvance - xOffset * incrScale;
                    }
                    // Since x0 has xAdvance baked into it, we need to zero out xAdvance.
                    // Since x0 will have offset baked into it, we need to counteract that
                    // in xAdvance. The next x position will be (x0 - xAdvance).
                    xAdvance = -xOffset * incrScale;
                } else {
                    x0 = bound.x;

                    // The first letter quad will be drawn right at the first
                    // position, but it does not offset by the character's offset
                    // amount.  This means that we've potentially accumulated extra
                    // pixels, and the next letter won't get drawn far enough unless
                    // we add this offset back into xAdvance, by subtracting it.
                    // This is the same thing that's done below, because we've
                    // technically baked the offset in just like below.  It doesn't
                    // look like it at first glance, so I'm keeping it separate with
                    // this comment.
                    xAdvance -= xOffset * incrScale;
                }
            } else {
               if (isRightToLeft()) {
                   // For RTL text the xAdvance of the current letter is deducted,
                   // while for LTR text the advance of the letter before is added.
                   x0 = previous.getNextX() - xAdvance - xOffset * incrScale;
                   // Since x0 has xAdvance baked into it, we need to zero out xAdvance.
                   // Since x0 will have offset baked into it we need to counteract that
                   // in xAdvance. The next x position will be (x0 - xAdvance)
                   xAdvance = - xOffset * incrScale;
                } else {
                    x0 = previous.getNextX() + xOffset * incrScale;
                   // Since x0 will have offset baked into it, we
                   // need to counteract that in xAdvance.  This is better
                   // than removing it in getNextX() because we also need
                   // to take kerning into account below, which will also
                   // get baked in.
                   // Without this, getNextX() will return values too far to
                   // the left, for example.
                   xAdvance -= xOffset * incrScale;
                }
            }
            y0 = lineY + LINE_DIR*yOffset;

            // Adjust for kerning
            BitmapCharacter lastChar = previous.getBitmapChar();
            if (lastChar != null && block.isKerning()) {
                kernAmount = lastChar.getKerning(c) * sizeScale;
                x0 += kernAmount * incrScale;
                 // Need to unbake the kerning from xAdvance since it
                // is baked into x0... see above.
                //xAdvance -= kernAmount * incrScale;
                // No, kerning is an inter-character spacing and _does_ affect
                // all subsequent cursor positions.
            }
        }
        if (isEndOfLine()) {
            xAdvance = bound.x-x0;
        }
    }

    /**
     * add temporary linewrap indicator
     */
    void setEndOfLine() {
        this.eol = true;
    }

    boolean isEndOfLine() {
        return eol;
    }

    boolean isLineWrap() {
        return !isHead() && !isTail() && bitmapChar == null && c == Character.MIN_VALUE;
    }

    private float computeLineY(StringBlock block) {
        if (isHead()) {
            return getBound(block).y;
        } else if (previous.eol) {
            return previous.getNextLine();
        } else {
            return previous.lineY;
        }
    }


    boolean isLineStart() {
        return x0 == 0 || (previous != null && previous.eol);
    }

    boolean isBlank() {
        return c == ' ' || isTab();
    }

    public void storeToArrays(float[] pos, float[] tc, short[] idx, byte[] colors, int quadIdx) {
        float x = x0+alignX;
        float y = y0-alignY;
        float xpw = x+width;
        float ymh = y-height;

        pos[0] = x;   pos[1]  = y;   pos[2]  = 0;
        pos[3] = x;   pos[4]  = ymh; pos[5]  = 0;
        pos[6] = xpw; pos[7]  = ymh; pos[8]  = 0;
        pos[9] = xpw; pos[10] = y;   pos[11] = 0;

        float v0 = 1f - this.v0;
        float v1 = 1f - this.v1;

        tc[0] = u0; tc[1] = v0;
        tc[2] = u0; tc[3] = v1;
        tc[4] = u1; tc[5] = v1;
        tc[6] = u1; tc[7] = v0;

        colors[3] = (byte) (colorInt & 0xff);
        colors[2] = (byte) ((colorInt >> 8) & 0xff);
        colors[1] = (byte) ((colorInt >> 16) & 0xff);
        colors[0] = (byte) ((colorInt >> 24) & 0xff);
        System.arraycopy(colors, 0, colors, 4,  4);
        System.arraycopy(colors, 0, colors, 8,  4);
        System.arraycopy(colors, 0, colors, 12, 4);

        short i0 = (short) (quadIdx * 4);
        short i1 = (short) (i0 + 1);
        short i2 = (short) (i0 + 2);
        short i3 = (short) (i0 + 3);

        idx[0] = i0; idx[1] = i1; idx[2] = i2;
        idx[3] = i0; idx[4] = i2; idx[5] = i3;
    }

    public void appendPositions(FloatBuffer fb) {
        float sx = x0+alignX;
        float sy = y0-alignY;
        float ex = sx+width;
        float ey = sy-height;
        // NOTE: subtracting the height here
        // because OGL's Ortho origin is at lower-left
        fb.put(sx).put(sy).put(0f);
        fb.put(sx).put(ey).put(0f);
        fb.put(ex).put(ey).put(0f);
        fb.put(ex).put(sy).put(0f);
    }

    public void appendPositions(ShortBuffer sb) {
        final float x1 = getX1();
        final float y1 = getY1();
        short x = (short) x0;
        short y = (short) y0;
        short xpw = (short) (x1);
        short ymh = (short) (y1);

        sb.put(x).put(y).put((short)0);
        sb.put(x).put(ymh).put((short)0);
        sb.put(xpw).put(ymh).put((short)0);
        sb.put(xpw).put(y).put((short)0);
    }

    public void appendTexCoords(FloatBuffer fb) {
        // flip coords to be compatible with OGL
        float v0 = 1 - this.v0;
        float v1 = 1 - this.v1;

        // upper left
        fb.put(u0).put(v0);
        // lower left
        fb.put(u0).put(v1);
        // lower right
        fb.put(u1).put(v1);
        // upper right
        fb.put(u1).put(v0);
    }

    public void appendColors(ByteBuffer bb) {
        bb.putInt(colorInt);
        bb.putInt(colorInt);
        bb.putInt(colorInt);
        bb.putInt(colorInt);
    }

    public void appendIndices(ShortBuffer sb, int quadIndex) {
        // each quad has 4 indices
        short v0 = (short) (quadIndex * 4);
        short v1 = (short) (v0 + 1);
        short v2 = (short) (v0 + 2);
        short v3 = (short) (v0 + 3);

        sb.put(v0).put(v1).put(v2);
        sb.put(v0).put(v2).put(v3);
//        sb.put(new short[]{ v0, v1, v2,
//                            v0, v2, v3 });
    }


    @Override
    public String toString() {
        return String.valueOf(c);
    }

    void setAlignment(float alignX, float alignY) {
        this.alignX = alignX;
        this.alignY = alignY;
    }

    float getAlignX() {
        return alignX;
    }

    float getAlignY() {
        return alignY;
    }

    boolean isLineFeed() {
        return c == '\n';
    }

    boolean isTab() {
        return c == '\t';
    }

}
