package com.jme3.font;

import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;

import com.jme3.math.ColorRGBA;

/**
 * LetterQuad contains the position, color, 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;

    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 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;
        }
        return x0 > 0 && bound.x+bound.width-gap < getX1();
    }
    
    float getX0() {
        return x0;
    }

    float getX1() {
        return x0+width;
    }
    
    float getNextX() {
        return 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 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;
            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();
            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) {
                x0 = bound.x;
                
                // The first letter quad will be drawn right at the first
                // position... but it does not offset by the characters 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 {
                x0 = previous.getNextX() + xOffset * incrScale;
                
                // Since x0 will have offset baked into it then 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';
    }
    
}
