/**
 *  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.arc;

import java.awt.BasicStroke;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.geom.Arc2D;
import java.awt.geom.GeneralPath;

import java.io.IOException;
import java.io.ObjectInputStream;

import net.sf.jaxodraw.object.line.JaxoSLine;
import net.sf.jaxodraw.util.JaxoUtils;
import net.sf.jaxodraw.util.graphics.JaxoGraphics2D;


/** A dashed arc.
 * @since 2.0
 */
public abstract class JaxoDashArc extends JaxoArcObject {
    private static final long serialVersionUID = 314159L;
    private transient BasicStroke innerStroke;
    private transient BasicStroke outerStroke;
    private transient Arc2D arc = new Arc2D.Double();

    private void readObject(final ObjectInputStream in)
        throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        arc = new Arc2D.Double();
        resetStroke();
    }

    /** {@inheritDoc} */
    public final void paint(final JaxoGraphics2D g2) {
        if (tooSingular(0.02d)) {
            return;
        }

        if (isOneLine()) {
            g2.setColor(getColor());
            g2.setStroke(getStroke());
            g2.draw(dashLine());

            if (isPaintArrow()) {
                paintArrow(g2);
            }

            return;
        }

        final double[] par = getArcParameters();

        g2.setColor(getColor());

        if (isDoubleLine()) {
            resetStroke(); // need to recalculate the strokes
            g2.setStroke(innerStroke);
            g2.draw(getInnerPath(par));
            g2.setStroke(outerStroke);
            g2.draw(getOuterPath(par));
        } else {
            g2.setStroke(getStroke());
            g2.draw(getObjectPath(par));
        }

        if (isPaintArrow()) {
            paintArrow(g2);
        }
    }

    private GeneralPath dashLine() {
        final GeneralPath gp = getGeneralPath();
        gp.reset();

        if (isDoubleLine()) {
            final float length = (float) Math.sqrt(((getX3() - getX()) * (getX3() - getX()))
                + ((getY3() - getY()) * (getY3() - getY())));
            final float sepx = (getY3() - getY()) / length * getDLSeparation() / 2.f;
            final float sepy = (getX3() - getX()) / length * getDLSeparation() / 2.f;
            gp.moveTo(getX() - sepx, getY() + sepy);
            gp.lineTo(getX3() - sepx, getY3() + sepy);
            gp.moveTo(getX() + sepx, getY() - sepy);
            gp.lineTo(getX3() + sepx, getY3() - sepy);
        } else {
            gp.moveTo(getX(), getY());
            gp.lineTo(getX3(), getY3());
        }

        return gp;
    }

    private GeneralPath getObjectPath(final double[] par) {
        final GeneralPath gp = getGeneralPath();
        gp.reset();
        arc.setArcByCenter(par[0], par[1], par[2], par[4], par[3], Arc2D.OPEN);
        gp.append(arc, false);
        return gp;
    }

    private GeneralPath getInnerPath(final double[] par) {
        final GeneralPath gp = getGeneralPath();
        gp.reset();
        arc.setArcByCenter(par[0], par[1], par[2] - getDLSeparation() / 2.f,
                par[4], par[3], Arc2D.OPEN);
        gp.append(arc, false);
        return gp;
    }

    private GeneralPath getOuterPath(final double[] par) {
        final GeneralPath gp = getGeneralPath();
        gp.reset();
        arc.setArcByCenter(par[0], par[1], par[2] + getDLSeparation() / 2.f,
                par[4], par[3], Arc2D.OPEN);
        gp.append(arc, false);
        return gp;
    }

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

        if (isSingular()) {
            // singular arc: return a Rectangle that just contains
            // the three arc points to avoid NPE
            bb = new Rectangle(getX(), getY(), 0, 0);
            bb.add(getX2(), getY2());
            bb.add(getX3(), getY3());

            return bb;
        }

        if (isOneLine()) {
            bb = getStroke().createStrokedShape(dashLine()).getBounds();

            if (isPaintArrow()) {
                bb.add(getArrow().getBounds(arrowCoordinates()));
            }

            return bb;
        }

        final double[] par = getArcParameters();

        if (isDoubleLine()) {
            bb = innerStroke.createStrokedShape(getInnerPath(par)).getBounds();
            bb.add(outerStroke.createStrokedShape(getOuterPath(par)).getBounds());
        } else {
            bb = getStroke().createStrokedShape(getObjectPath(par)).getBounds();
        }

        if (isPaintArrow()) {
            bb.add(getArrow().getBounds(arrowCoordinates()));
        }

        return bb;
    }

    /** {@inheritDoc} */
    public final String latexCommand(final float scale, final Dimension canvasDim) {
        if (isSingular()) {
            return "% Singular DashArc, ignored!" + JaxoUtils.LINE_SEPARATOR;
        }

        if (isOneLine()) {
            final JaxoSLine line = new JaxoSLine();
            line.setLocation(getX(), getY(), getX3(), getY3());
            line.setStrokeWidth(getStrokeWidth());
            line.setDash(getDash());
            line.setArrow(getArrow().copy());
            line.setArrowPosition(getArrowPosition());
            line.setPaintArrow(isPaintArrow());
            line.setFlip(isFlip());
            line.setDoubleLine(isDoubleLine());
            line.setDLSeparation(getDLSeparation());
            return line.latexCommand(scale, canvasDim);
        }

        final double[] par = getArcParameters();
        final double cx = par[0] / scale;
        final double cy = (canvasDim.height - par[1]) / scale;
        final double r = par[2] / scale;

        final double sa = par[4];
        final double ea = par[3] + par[4];
        final boolean clockwise = (par[3] < 0.d);

        final String options = getOptionsCommand(scale, clockwise);

        return "\\Arc" + options + "(" + D_FORMAT.format(cx) + ","
                + D_FORMAT.format(cy) + ")" + "(" + D_FORMAT.format(r) + ","
                + D_FORMAT.format(sa) + "," + D_FORMAT.format(ea) + ")";
    }

    /** {@inheritDoc} */
    @Override
    protected void resetStroke() {
        if (JaxoUtils.zero(getStrokeWidth()) || JaxoUtils.zero(getDash())) {
            // during initialization, either of them is still null
            // so wait until both are set
            return;
        }

        if (isSingular()) {
            return;
        }

        if (isOneLine()) {
            setStroke(new BasicStroke(getStrokeWidth(), BasicStroke.CAP_ROUND,
                BasicStroke.JOIN_MITER, 10.0f, strokeDashes(), 0.0f));
            this.innerStroke =
                new BasicStroke(getStrokeWidth(), BasicStroke.CAP_ROUND,
                    BasicStroke.JOIN_MITER, 10.0f, strokeDashes(), 0.0f);
            this.outerStroke =
                new BasicStroke(getStrokeWidth(), BasicStroke.CAP_ROUND,
                    BasicStroke.JOIN_MITER, 10.0f, strokeDashes(), 0.0f);
            return;
        }

        double radius = getRadius();
        if (JaxoUtils.zero(radius)) {
            radius = 1.d;
        }

        setStroke(new BasicStroke(getStrokeWidth(), BasicStroke.CAP_ROUND,
                BasicStroke.JOIN_MITER, 10.0f, strokeDashes(), 0.0f));
        this.innerStroke =
            new BasicStroke(getStrokeWidth(), BasicStroke.CAP_ROUND,
                BasicStroke.JOIN_MITER, 10.0f, innerStrokeDashes(radius), 0.0f);
        this.outerStroke =
            new BasicStroke(getStrokeWidth(), BasicStroke.CAP_ROUND,
                BasicStroke.JOIN_MITER, 10.0f, outerStrokeDashes(radius), 0.0f);
    }

    /**
     * Returns an array that is used as the dash parameter in
     * {@link java.awt.BasicStroke} to paint this object.
     * @return a dash array.
     */
    protected abstract float[] strokeDashes();

    /**
     * Returns an array that is used as the dash parameter in
     * {@link java.awt.BasicStroke} to paint the inner part of
     * this arc in double-line mode.
     * @param radius the radius.
     * @return a dash array.
     */
    protected abstract float[] innerStrokeDashes(double radius);

    /**
     * Returns an array that is used as the dash parameter in
     * {@link java.awt.BasicStroke} to paint the outer part of
     * this arc in double-line mode.
     * @param radius the radius.
     * @return a dash array.
     */
    protected abstract float[] outerStrokeDashes(double radius);

        // Get the axodraw4j options set for this line

    private String getOptionsCommand(final float scale, final boolean clockwise) {

        final float dashSize = getDash() / scale;
        final StringBuffer optioncmd = new StringBuffer("dash,dashsize=").append(
            D_FORMAT.format(dashSize));

        if (isPaintArrow()) {
            final float arpos = getArrowPosition();
            final String arrowcmd = arrowCmnd(arpos, scale);

            optioncmd.append(',').append(arrowcmd);

            if (isDoubleLine()) {
                optioncmd.append(",double,sep=").append(
                    D_FORMAT.format(this.getDLSeparation()));
            }

            if (clockwise) {
                optioncmd.append(",clock");
            }

        } else if (isDoubleLine()) {
            optioncmd.append(",double,sep=").append(
                D_FORMAT.format(this.getDLSeparation()));

            if (clockwise) {
                optioncmd.append(",clock");
            }

        } else if (clockwise) {
            optioncmd.append(",clock");
        }

        optioncmd.insert(0, '[').append(']');

        return optioncmd.toString();
    }

    private String arrowCmnd(final float arpos, final float scale) {
        final StringBuffer arrowcmd = new StringBuffer(this.getArrow().latexCommand(arpos, scale));
        if (isFlip()) {
            arrowcmd.append(",flip");
        }
        return arrowcmd.toString();
    }
}
