/*
 * 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.font.BitmapFont.Align;
import com.jme3.font.BitmapFont.VAlign;
import com.jme3.font.ColorTags.Range;
import com.jme3.math.ColorRGBA;
import java.util.LinkedList;

/**
 * Manage and align LetterQuads
 * @author YongHoon
 */
class Letters {
    private final LetterQuad head;
    private final LetterQuad tail;
    private final BitmapFont font;
    private LetterQuad current;
    private StringBlock block;
    private float totalWidth;
    private float totalHeight;
    private ColorTags colorTags = new ColorTags();
    private ColorRGBA baseColor = null;
    private float baseAlpha = -1;
    private String plainText;

    Letters(BitmapFont font, StringBlock bound, boolean rightToLeft) {
        final String text = bound.getText();
        this.block = bound;
        this.font = font;
        head = new LetterQuad(font, rightToLeft);
        tail = new LetterQuad(font, rightToLeft);
        setText(text);
    }

    void setText(final String text) {
        colorTags.setText(text);
        plainText = colorTags.getPlainText();

        head.setNext(tail);
        tail.setPrevious(head);
        current = head;
        if (text != null && plainText.length() > 0) {
            LetterQuad l = head;
            for (int i = 0; i < plainText.length(); i++) {
                l = l.addNextCharacter(plainText.charAt(i));
                if (baseColor != null) {
                    // Give the letter a default color if
                    // one has been provided.
                    l.setColor( baseColor );
                }
            }
        }

        LinkedList<Range> ranges = colorTags.getTags();
        if (!ranges.isEmpty()) {
            for (int i = 0; i < ranges.size()-1; i++) {
                Range start = ranges.get(i);
                Range end = ranges.get(i+1);
                setColor(start.start, end.start, start.color);
            }
            Range end = ranges.getLast();
            setColor(end.start, plainText.length(), end.color);
        }

        invalidate();
    }

    LetterQuad getHead() {
        return head;
    }

    LetterQuad getTail() {
        return tail;
    }

    void update() {
        LetterQuad l = head;
        int lineCount = 1;
        BitmapCharacter ellipsis = font.getCharSet().getCharacter(block.getEllipsisChar());
        float ellipsisWidth = ellipsis!=null? ellipsis.getWidth()*getScale(): 0;

        while (!l.isTail()) {
            if (l.isInvalid()) {
                l.update(block);

                if (l.isInvalid(block)) {
                    switch (block.getLineWrapMode()) {
                    case Character:
                        lineWrap(l);
                        lineCount++;
                        break;
                    case Word:
                        if (!l.isBlank()) {
                            // search last blank character before this word
                            LetterQuad blank = l;
                            while (!blank.isBlank()) {
                                if (blank.isLineStart() || blank.isHead()) {
                                    lineWrap(l);
                                    lineCount++;
                                    blank = null;
                                    break;
                                }
                                blank = blank.getPrevious();
                            }
                            if (blank != null) {
                                blank.setEndOfLine();
                                lineCount++;
                                while (blank != l) {
                                    blank = blank.getNext();
                                    blank.invalidate();
                                    blank.update(block);
                                }
                            }
                        }
                        break;
                    case NoWrap:
                        LetterQuad cursor = l.getPrevious();
                        while (cursor.isInvalid(block, ellipsisWidth) && !cursor.isLineStart()) {
                            cursor = cursor.getPrevious();
                        }
                        cursor.setBitmapChar(ellipsis);
                        cursor.update(block);
                        cursor = cursor.getNext();
                        while (!cursor.isTail() && !cursor.isLineFeed()) {
                            cursor.setBitmapChar(null);
                            cursor.update(block);
                            cursor = cursor.getNext();
                        }
                        break;
                    case Clip:
                        // Clip the character that falls out of bounds
                        l.clip(block);

                        // Clear the rest up to the next line feed.
                        for( LetterQuad q = l.getNext(); !q.isTail() && !q.isLineFeed(); q = q.getNext() ) {
                            q.setBitmapChar(null);
                            q.update(block);
                        }
                        break;
                    }
                }
            } else if (current.isInvalid(block)) {
                invalidate(current);
            }
            if (l.isEndOfLine()) {
                lineCount++;
            }
            l = l.getNext();
        }

        align();
        block.setLineCount(lineCount);
        rewind();
    }

    private void align() {
        final Align alignment = block.getAlignment();
        final VAlign valignment = block.getVerticalAlignment();
        if (block.getTextBox() == null || (alignment == Align.Left && valignment == VAlign.Top))
            return;
        LetterQuad cursor = tail.getPrevious();
        cursor.setEndOfLine();
        final float width = block.getTextBox().width;
        final float height = block.getTextBox().height;
        float lineWidth = 0;
        float gapX = 0;
        float gapY = 0;
        validateSize();
        if (totalHeight < height) { // align vertically only for no overflow
            switch (valignment) {
            case Top:
                gapY = 0;
                break;
            case Center:
                gapY = (height-totalHeight)*0.5f;
                break;
            case Bottom:
                gapY = height-totalHeight;
                break;
            }
        }
        while (!cursor.isHead()) {
            if (cursor.isEndOfLine()) {
                lineWidth = cursor.getX1()-block.getTextBox().x;
                if (alignment == Align.Center) {
                    gapX = (width-lineWidth)/2;
                } else if (alignment == Align.Right) {
                    gapX = width-lineWidth;
                } else {
                    gapX = 0;
                }
            }
            cursor.setAlignment(gapX, gapY);
            cursor = cursor.getPrevious();
        }
    }

    private void lineWrap(LetterQuad l) {
        if (l.isHead() || l.isBlank())
            return;
        l.getPrevious().setEndOfLine();
        l.invalidate();
        l.update(block); // TODO: update from l
    }

    float getCharacterX0() {
        return current.getX0();
    }

    float getCharacterY0() {
        return current.getY0();
    }

    float getCharacterX1() {
        return current.getX1();
    }

    float getCharacterY1() {
        return current.getY1();
    }

    float getCharacterAlignX() {
        return current.getAlignX();
    }

    float getCharacterAlignY() {
        return current.getAlignY();
    }

    float getCharacterWidth() {
        return current.getWidth();
    }

    float getCharacterHeight() {
        return current.getHeight();
    }

    public boolean nextCharacter() {
        if (current.isTail())
            return false;
        current = current.getNext();
        return true;
    }

    public int getCharacterSetPage() {
        return current.getBitmapChar().getPage();
    }

    public LetterQuad getQuad() {
        return current;
    }

    public void rewind() {
        current = head;
    }

    public void invalidate() {
        invalidate(head);
    }

    public void invalidate(LetterQuad cursor) {
        totalWidth = -1;
        totalHeight = -1;

        while (!cursor.isTail() && !cursor.isInvalid()) {
            cursor.invalidate();
            cursor = cursor.getNext();
        }
    }

    float getScale() {
        return block.getSize() / font.getCharSet().getRenderedSize();
    }

    public boolean isPrintable() {
        return current.getBitmapChar() != null;
    }

    float getTotalWidth() {
        validateSize();
        return totalWidth;
    }

    float getTotalHeight() {
        validateSize();
        return totalHeight;
    }

    void validateSize() {
        if (totalWidth < 0) {
            LetterQuad l = head;
            while (!l.isTail()) {
                totalWidth = Math.max(totalWidth, l.getX1());
                l = l.getNext();
                totalHeight = Math.max(totalHeight, -l.getY1());
            }
        }
    }

    /**
     * @param start start index to set style. inclusive.
     * @param end   end index to set style. EXCLUSIVE.
     * @param style
     */
    void setStyle(int start, int end, int style) {
        LetterQuad cursor = head.getNext();
        while (!cursor.isTail()) {
            if (cursor.getIndex() >= start && cursor.getIndex() < end) {
                cursor.setStyle(style);
            }
            cursor = cursor.getNext();
        }
    }

    /**
     * Sets the base color for all new letter quads and resets
     * the color of existing letter quads.
     */
    void setColor( ColorRGBA color ) {
        baseColor = color;
        colorTags.setBaseColor(color);
        setColor( 0, block.getText().length(), color );
    }

    ColorRGBA getBaseColor() {
        return baseColor;
    }

    /**
     * @param start start index to set style. inclusive.
     * @param end   end index to set style. EXCLUSIVE.
     * @param color
     */
    void setColor(int start, int end, ColorRGBA color) {
        LetterQuad cursor = head.getNext();
        while (!cursor.isTail()) {
            if (cursor.getIndex() >= start && cursor.getIndex() < end) {
                cursor.setColor(color);
            }
            cursor = cursor.getNext();
        }
    }

    float getBaseAlpha() {
        return baseAlpha;
    }

    void setBaseAlpha( float alpha ) {        this.baseAlpha = alpha;
        colorTags.setBaseAlpha(alpha);

        if (alpha == -1) {
            alpha = baseColor != null ? baseColor.a : 1;
        }

        // Forward the new alpha to the letter quads
        LetterQuad cursor = head.getNext();
        while (!cursor.isTail()) {
            cursor.setAlpha(alpha);
            cursor = cursor.getNext();
        }

        // If the alpha was reset to "default", ie: -1
        // then the color tags are potentially reset and
        // we need to reapply them.  This has to be done
        // second since it may override any alpha values
        // set above... but you still need to do the above
        // since non-color tagged text is treated differently
        // even if part of a color tagged string.
        if (baseAlpha == -1) {
            LinkedList<Range> ranges = colorTags.getTags();
            if (!ranges.isEmpty()) {
                for (int i = 0; i < ranges.size()-1; i++) {
                    Range start = ranges.get(i);
                    Range end = ranges.get(i+1);
                    setColor(start.start, end.start, start.color);
                }
                Range end = ranges.getLast();
                setColor(end.start, plainText.length(), end.color);
            }
        }

        invalidate();
    }

}
