/**
 *  Licensed under GPL. For more information, see
 *    http://jaxodraw.sourceforge.net/license.html
 *  or the LICENSE file in the jaxodraw distribution.
 */
package net.sf.jaxodraw.object.text;

import java.awt.Dimension;
import java.awt.Font;
import java.awt.Rectangle;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import net.sf.jaxodraw.object.JaxoObject;
import net.sf.jaxodraw.object.JaxoObjectEditPanel;
import net.sf.jaxodraw.util.JaxoPrefs;
import net.sf.jaxodraw.util.JaxoColor;
import net.sf.jaxodraw.util.JaxoGeometry;
import net.sf.jaxodraw.util.JaxoGreek;
import net.sf.jaxodraw.util.graphics.JaxoGraphics2D;


/** Defines a JaxoObject which is a postscript text.
 *  Also handles sub- and superscripts, as well as a set of greek characters
 *  via a syntax that is derived from LaTeX.
 * @since 2.0
 */
public class JaxoPSText extends JaxoTextObject {
    private static final long serialVersionUID = 314159L;
    private static final List<Integer> FONT_SIZES;

    static {
        final List<Integer> fSizes = new ArrayList<Integer>(16);

        fSizes.add(6);
        fSizes.add(8);
        fSizes.add(10);
        fSizes.add(12);
        fSizes.add(14);
        fSizes.add(18);
        fSizes.add(24);
        fSizes.add(36);
        fSizes.add(48);
        fSizes.add(60);
        fSizes.add(72);

        FONT_SIZES = Collections.unmodifiableList(fSizes);
    }

    private static final List<Integer> FONT_STYLES;

    static {
        final List<Integer> fStyles = new ArrayList<Integer>(4);

        fStyles.add(Font.PLAIN);
        fStyles.add(Font.BOLD);
        fStyles.add(Font.ITALIC);
        fStyles.add(Font.BOLD | Font.ITALIC);

        FONT_STYLES = Collections.unmodifiableList(fStyles);
    }

    private static final String LATEX_COMMAND = "% There is a postscript text here!";

    /** The font of this JaxoPSText object.*/
    private Font textFont;

    // Elements to paint; may be 'null', created when
    // needed by ensureParsed().
    private transient List<Element> elements;

    // Bounding box, relative to getX(), getY() and not rotated
    // (Calculation of the bounding box is expensive, so avoid
    // having to do it when these properties change.)
    private transient Rectangle boundingBox;

    /**
     * List of numbers of default font sizes to display to the user.
     *
     * @return List of Integers.
     * @see Font#getStyle
     * @see Font#deriveFont(int)
     */
    public static List<Integer> getFontSizeDefaults() {
        return FONT_SIZES;
    }

    /**
     * List of Integer font styles to display to the user.
     *
     * @return List of Integers.
     */
    public static List<Integer> getFontStyles() {
        return FONT_STYLES;
    }
      //
     // Bean getter and setter methods
    //

    /** Returns the textFont property of this text object.
     * @return The textFont property of this text object.
     */
    public final Font getFont() {
        return textFont;
    }

    /** Sets the textFont property of this text object.
     * @param font The textFont property of this text object.
     */
    public final void setFont(final Font font) {
        final Font old = textFont;
        this.textFont = font;
        firePropertyChange("font", old, textFont);

        elements = null;
    }

    /** {@inheritDoc} */
    @Override
    public void setTextString(final String string) {
        super.setTextString(string);
        elements = null;
    }

      //
     // Other setters
    //

    /**
     * Sets the font name of this text. If the current font is null,
     * the style and size of the font are taken from the preferences.
     *
     * @param name the font name to set.
     */
    public final void setFontName(final String name) {
        setFont(new Font(name, getFont().getStyle(), getFont().getSize()));
    }

    /**
     * Sets the font style of this text. If the current font is null,
     * the name and size of the font are taken from the preferences.
     *
     * @param style the font style to set.
     */
    public final void setFontStyle(final int style) {
        setFont(new Font(getFont().getName(), style, getFont().getSize()));
    }

    /**
     * Sets the font size of this text. If the current font is null,
     * the style and name of the font are taken from the preferences.
     *
     * @param size the font size to set.
     */
    public final void setFontSize(final int size) {
        setFont(new Font(getFont().getName(), getFont().getStyle(), size));
    }

    /** Returns an exact copy of this JaxoPSText.
     * @return A copy of this JaxoPSText.
     */
    @Override
    public final JaxoObject copy() {
        final JaxoPSText result = (JaxoPSText) super.copy();

        if (elements != null) {
            result.elements = new ArrayList<Element>(elements);
        }

        return result;
    }

    /** Sets all parameters from the given object to the current one.
     * @param temp The object to copy from.
     */
    public void copyFrom(final JaxoPSText temp) {
        super.copyFrom(temp);
        final Font font = temp.getFont();
        setFont(new Font(font.getName(), font.getStyle(), font.getSize()));
    }

    /** {@inheritDoc} */
    @Override
    public void setState(final JaxoObject o) {
        if (o instanceof JaxoPSText) {
            copyFrom((JaxoPSText) o);
        } else {
            throw new UnsupportedOperationException("Cannot copy from super type!");
        }
    }

    /** {@inheritDoc} */
    @Override
    public final boolean isCopy(final JaxoObject comp) {
        boolean isCopy = false;

        if (comp instanceof JaxoPSText) {
            final JaxoPSText text = (JaxoPSText) comp;
            if (text.getFont().equals(getFont()) && super.isCopy(text)) {
                isCopy = true;
            }
        }

        return isCopy;
    }


    /** {@inheritDoc} */
    public final void paint(final JaxoGraphics2D g2) {
        ensureParsed();

        g2.setColor(getColor());

        final double theta = Math.toRadians(getRotationAngle());
        final double x = (double) getX();
        final double y = (double) getY();

        g2.rotate(theta, x, y);

        for (final Iterator<Element> i = elements.iterator(); i.hasNext();) {
            final Element t = i.next();

            g2.setFont(t.getFont());
            g2.drawString(t.getText(), (float) (getX() + t.getX()), (float) (getY() + t.getY()));
        }

        g2.rotate(-theta, x, y);
    }

    /**
     * Returns the bounding box of this object.
     *
     * @return the bounding box of this object.
     */
    public Rectangle getBounds() {
        ensureParsed();

        final double theta = Math.toRadians(getRotationAngle());

        if (theta == 0) {
            final Rectangle result = boundingBox.getBounds();
            result.x += getX();
            result.y += getY();
            return result;
        } else {
            final GeneralPath gp = new GeneralPath(boundingBox);
            final AffineTransform at = AffineTransform.getRotateInstance(theta);
            gp.transform(at);
            gp.transform(AffineTransform.getTranslateInstance(getX(), getY()));

            return gp.getBounds();
        }
    }

    /** {@inheritDoc} */
    public final String latexCommand(final float scale, final Dimension canvasDim) {
        return LATEX_COMMAND;
    }

    /** {@inheritDoc} */
    public void prepareEditPanel(final JaxoObjectEditPanel editPanel) {
        editPanel.addPositionPanel(getX(), getY(), 0, 0);
        editPanel.addRotationPanel(getRotationAngle(), 1, 0);
        editPanel.addPSFontPanel(getFont(), 0, 1, 2);
        editPanel.addColorPanel(getColor(), JaxoObjectEditPanel.TYPE_COLOR, 0, 2);
        editPanel.addTextPanel(getTextString(), 1, 2);

        editPanel.setTitleAndIcon("Text_parameters", "font_truetype.png");
    }

    /** {@inheritDoc} */
    @Override
    public final void rescaleObject(final int orx, final int ory, final float scale) {
        super.rescaleObject(orx, ory, scale);

        final float textSize = textFont.getSize2D();

        float rescaledTextSize = textSize * scale;

        if (rescaledTextSize < 2) {
            rescaledTextSize = 2;
        } else if (rescaledTextSize > 286) {
            rescaledTextSize = 286;
        }

        setFont(textFont.deriveFont(rescaledTextSize));
    }

    /** {@inheritDoc} */
    @Override
    public void setPreferences() {
        super.setPreferences();
        setRotationAngle(JaxoPrefs.getIntPref(JaxoPrefs.PREF_PSROTANGLE));
        setColor(JaxoColor.getColor(JaxoPrefs.getStringPref(
                    JaxoPrefs.PREF_TEXTCOLOR),
                JaxoPrefs.getIntPref(JaxoPrefs.PREF_COLORSPACE)));
        setFont(new Font(JaxoPrefs.getStringPref(
                    JaxoPrefs.PREF_PSFAMILY),
                JaxoPrefs.getIntPref(JaxoPrefs.PREF_PSSTYLE),
                JaxoPrefs.getIntPref(JaxoPrefs.PREF_PSSIZE)));
        setTextString("");
    }


    private void ensureParsed() {
        if (elements == null) {
            new Parser().parse(getTextString());
        }
    }


    /** An element is a chunk of text with a common font size
        placed a specific location (measured relative the JaxoObject location,
        and not taking into account a rotation).
    */
    private static class Element {
        private Font font;
        private String text;
        private double x, y;

        // only used during parsing
        private final double previousY;

        Element(final double newx, final double newy) {
            this.x = newx;
            this.y = newy;
            this.previousY = newy;
        }

        public Font getFont() {
            return font;
        }

        public void setFont(final Font newFont) {
            this.font = newFont;
        }

        public String getText() {
            return text;
        }

        public void setText(final String newText) {
            this.text = newText;
        }

        public double getX() {
            return x;
        }

        public void setX(final double newX) {
            this.x = newX;
        }

        public double getY() {
            return y;
        }

        public void setY(final double newY) {
            this.y = newY;
        }

        public double getPreviousY() {
            return previousY;
        }
    }

    /** Parser for the text string, handling greek letters and super/subscripts.
        The syntax is the following:
        <pre>
        \\greek    the correcting greek letter {@see JaxoGreek}
        {}         ignore, used to separate adjacent tokens
        {+char     ignore {, take character
                   (usage deprecated because the bracket is not taken into account for matching).
        \\{        literal {
        \\}        literal }
        \\_        literal _
        \\^        literal ^
        \\\\       literal \\
        \\+char    ignore \\, take character
        _{         start subscript, terminated at the matching }
        ^{         start superscript, terminated at the matching {
        }          terminates a sub/superscript; ignored if not within any
        char       the character itself
        </pre>
     */
    private class Parser {
        private FontRenderContext renderContext;
        private double height;
        private double x;
        private Rectangle2D bounds;
        private Element element;
        private int level;

        /** Parse 'textString', filling 'elements'.*/
        public void parse(final String text) {
            // The Bounding box is measured using a default render context.
            // The resulting subpixel inaccuracy is corrected by adding one pixel
            // one each side of the resulting bounding box
            // (Painting, then, happens using the Graphics
            // FontRenderContext.)
            renderContext = new FontRenderContext(
                    new AffineTransform(), true, true);

            // Calculate height to determine raising/lowering from font size
            height = getFont().getLineMetrics("A", renderContext).getHeight();

            x = 0;
            level = 0;
            bounds = new Rectangle2D.Double();

            String textString = text;

            if (textString == null) {
                textString = "";
            }

            elements = new ArrayList<Element>(8);

            element = new Element(x, 0);
            final StringBuffer elementText = new StringBuffer(64);

            for (int i = 0; i < textString.length(); i++) {
                final char ch = textString.charAt(i);
                final boolean last = i == textString.length() - 1;

                switch (ch) {
                    case '_':
                        if (last || textString.charAt(i + 1) != '{') {
                            elementText.append(ch);
                        } else {
                            finishElement(elementText.toString());
                            level++;
                            element.setY(element.getY()
                                    + 0.5 * Math.pow(2. / 3., level) * height);
                        }
                        break;
                    case '^':
                        if (last || textString.charAt(i + 1) != '{') {
                            elementText.append(ch);
                        } else {
                            finishElement(elementText.toString());
                            level++;
                            element.setY(element.getY()
                                    - 0.5 * Math.pow(2. / 3., level) * height);
                        }

                        break;
                    case '\\':
                        if (last) {
                            elementText.append(ch);
                        } else {
                            final char d = textString.charAt(i + 1);

                            // Literal special characters
                            if (d == '{' || d == '}' || d == '\\'
                                || d == '_' || d == '^') {
                                elementText.append(d);
                                i++;
                            } else {
                                int k = i + 1;
                                for (; k < textString.length(); k++) {
                                    final char t = textString.charAt(k);

                                    // assuming Greek characters only
                                    // contain [A-Za-z]
                                    if (!((t >= 'a' && t <= 'z')
                                       || (t >= 'A' && t <= 'Z'))) {
                                        break;
                                    }
                                }

                                if (k > i + 1) {
                                    elementText.append(JaxoGreek
                                    .getCharacter(
                                        textString.substring(i, k)));

                                    i = k - 1;
                                }
                                // else ignore backslash before non-greek
                            }
                        }
                        break;
                    case '{':
                        // ignore, use '\{' for literal {
                        // if followed by '}', ignore both;
                        // used for token separation
                        if (!last && textString.charAt(i + 1) == '}') {
                            i++;
                        }
                        break;
                    case '}':
                        if (level > 0) {
                            final double y = element.getPreviousY();
                            finishElement(elementText.toString());
                            element.setY(y);

                            --level;
                        }
                        // ignore if it does not close
                        break;
                    default:
                        elementText.append(ch);
                        break;
                }
            }

            finishElement(elementText.toString());

            boundingBox = bounds.getBounds();
            boundingBox.grow(2, 2);
        }


        private void finishElement(final String elementText) {
            if (elementText.length() > 0) {
                element.setText(elementText);
                element.setFont(getFont().deriveFont(
                        (float) (getFont().getSize2D() * Math.pow(2. / 3., level))));

                elements.add(element);

                final TextLayout l = new TextLayout(element.getText(), element.getFont(),
                    renderContext);
                final Rectangle2D.Double r = new Rectangle2D.Double();
                r.setRect(l.getBounds());
                r.x += element.getX();
                r.y += element.getY();
                JaxoGeometry.add(bounds, r);

                x += l.getAdvance();
            }

            // By default, the new element is placed at the same 'y' as the old one.
            // This is corrected later.
            element = new Element(x, element.getY());
        }
    }
}
