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

import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;

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

import java.util.ArrayList;
import java.util.List;

import net.sf.jaxodraw.object.JaxoObject;
import net.sf.jaxodraw.object.JaxoObjectEditPanel;
import net.sf.jaxodraw.object.JaxoWiggleObject;
import net.sf.jaxodraw.util.JaxoUtils;
import net.sf.jaxodraw.util.graphics.JaxoGraphics2D;


/**
 * Implementation of a bezier for photon lines.
 *
 * @since 2.0
 */
public class JaxoPBezier extends JaxoBezierObject implements JaxoWiggleObject {
    private static final long serialVersionUID = 2L;
    private transient float freq;
    private transient Point2D b2;
    private transient List<Double> renormsteps;

    private static final int TOLERANCE = 1;

    /** Constructor: just calls super(). */
    public JaxoPBezier() {
        super();
        initParams();
    }

    private void readObject(final ObjectInputStream in)
        throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        initParams();
    }

    private void initParams() {
        freq = 0.f;
        b2 = new Point2D.Double();
        renormsteps = new ArrayList<Double>(50);
    }

    /** Returns an exact copy of this JaxoPBezier.
     * @return A copy of this JaxoPBezier.
     */
    @Override
    public final JaxoObject copy() {
        final JaxoPBezier temp = new JaxoPBezier();
        temp.copyFrom(this);
        return temp;
    }

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

        if (comp instanceof JaxoPBezier) {
            isCopy = super.isCopy(comp);
        }

        return isCopy;
    }

    /** Sets all parameters from the given object to the current one.
     * @param temp The object to copy from.
     */
    public void copyFrom(final JaxoPBezier temp) {
        super.copyFrom(temp);
        this.freq = temp.getFrequency();
    }

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

    /** {@inheritDoc} */
    public final void paint(final JaxoGraphics2D g2) {
        g2.setColor(getColor());
        g2.setStroke(getStroke());
        g2.draw(getObjectPath());
    }

    /**
     * Returns the bounding box of this object.
     *
     * @return the bounding box of this object.
     */
    public Rectangle getBounds() {
        // use Area for bounding box calculation instead of gp.getBounds(),
        // see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4599407
        return getStroke().createStrokedShape(new Area(getObjectPath()))
                   .getBounds();
    }

    /** {@inheritDoc} */
    public final String latexCommand(final float scale, final Dimension canvasDim) {
        if (JaxoUtils.zero(freq)) {
            setFrequencyFromWiggles(getWiggles());
        } else {
            setWigglesFromFrequency();
        }

        initRenormSteps();

        final String command = constructLatexString(scale, canvasDim.height);

        // Construct the JDidentifier for import purposes

        final String jdIdentifier = "%JaxoID:PBez"
                + "(" + getX() + "," + getY() + ")" + "(" + getX2() + "," + getY2() + ")"
                + "(" + getX3() + "," + getY3() + ")" + "(" + getX4() + "," + getY4() + ")"
                + "{" + getAmp() + "}" + "{" + getWiggles() + "}" + "[" + isNoFreqStretching() + "]";

        return command + jdIdentifier;
    }

    /** {@inheritDoc} */
    public float getFrequency() {
        return freq;
    }

    /** {@inheritDoc} */
    public void setWigglesFromFrequency(final float frequency) {
        this.freq = frequency;
        setWigglesFromFrequency();
    }

    /** {@inheritDoc} */
    public void setWigglesFromFrequency() {
        final int n = (int) Math.round(getBezierLength() * getFrequency());
        setWiggles(n);
    }

    /** {@inheritDoc} */
    public void setFrequencyFromWiggles(final int wiggles) {
        setWiggles(wiggles);
        this.freq = (float) (wiggles / getBezierLength());
    }

    /** {@inheritDoc} */
    @Override
    public void setPreferences() {
        super.setPreferences();
        setWigglesFromFrequency(PHOTON_FREQ);
    }

    /** {@inheritDoc} */
    public void prepareEditPanel(final JaxoObjectEditPanel editPanel) {
        editPanel.add4PointsPanel(getPoints(), 0, 0, 3);
        editPanel.addLineColorPanel(getColor(), 3, 0);
        //editPanel.addDoubleLinePanel(bezier, 3, 0);
        editPanel.addStrokePanel(getStrokeWidth(), 0, 1);
        editPanel.addWigglePanel(getAmp(), getWiggles(), 1, 1);
        editPanel.addStretchingPanel(isNoFreqStretching(), 2, 1);

        editPanel.setTitleAndIcon("Photon_bezier_parameters", "bezierph.png");
    }

      //
     // private methods
    //

    private double setNextPoint(final Point2D b1, final double par, final double tstep, final boolean noFreqStretching) {
        double tpar = par;

        tryNextBezierPoint(tpar);

        if (noFreqStretching) {
            double length = b2.distance(b1);
            final double reflength = 1.d / getFrequency() / 2.d;

            if ((length - reflength) > TOLERANCE) {
                double rtpar;
                int alter = -1;
                for (int l = 1; l < 20; l++) {
                    rtpar = tpar + ((alter * tstep) / Math.pow(2.d, l));
                    tryNextBezierPoint(rtpar);
                    length = b2.distance(b1);
                    if (Math.abs(length - reflength) <= TOLERANCE) {
                        tpar = rtpar;
                        break;
                    } else if ((length - reflength) > TOLERANCE) {
                        alter = -1;
                        tpar = rtpar;
                    } else {
                        tpar = rtpar;
                        alter = +1;
                    }
                }
            } else if ((length - reflength) < -TOLERANCE) {
                double rtpar;
                int alter = -1;
                boolean seconditer = true;

                //find step for overshooting
                for (int l = 1; l < 10; l++) {
                    rtpar = tpar + (l * tstep);
                    tryNextBezierPoint(rtpar);
                    length = b2.distance(b1);
                    if (Math.abs(length - reflength) <= TOLERANCE) {
                        seconditer = false;
                        tpar = rtpar;
                        break;
                    } else if ((length - reflength) > TOLERANCE) {
                        seconditer = true;
                        alter = -1;
                        tpar = rtpar;
                        break;
                    } else {
                        tpar = rtpar;
                    }
                }

                if (seconditer) {
                    for (int l = 1; l < 20; l++) {
                        rtpar = tpar + ((alter * tstep) / Math.pow(2.d, l));
                        tryNextBezierPoint(rtpar);
                        length = b2.distance(b1);
                        if (Math.abs(length - reflength) <= TOLERANCE) {
                            tpar = rtpar;
                            break;
                        } else if ((length - reflength) > TOLERANCE) {
                            alter = -1;
                            tpar = rtpar;
                        } else {
                            tpar = rtpar;
                            alter = +1;
                        }
                    }
                }
            }
        }
        renormsteps.add(Double.valueOf(tpar));

        return tpar;
    }

    private void tryNextBezierPoint(final double t) {
        b2.setLocation(getPointOnCurve(t));
    }

    private GeneralPath getObjectPath() {
        if (JaxoUtils.zero(freq)) {
            setFrequencyFromWiggles(getWiggles());
        } else {
            setWigglesFromFrequency();
        }

        initRenormSteps();

        return drawingRoutine();
    }

    // drawingRoutine() almost duplicates constructLatexString(), simplify?
    private GeneralPath drawingRoutine() {
        final GeneralPath gp = getGeneralPath();
        gp.reset();

        final Point2D b1 = new Point2D.Double(getX(), getY());
        double bxp1, byp1, bxp2, byp2, lx, ly, theta;
        double arot = Math.PI / 2;
        final double c = 4.d / (3.d * Math.PI);
        final double amp =   (2.d / 3.d) * getAmp();
        final Point2D parallel = new Point2D.Double();
        final Point2D perp1 = new Point2D.Double();
        final Point2D perp2 = new Point2D.Double();

        final AffineTransform at = new AffineTransform();

        for (int t = 0; t < renormsteps.size(); t++) {
            tryNextBezierPoint(renormsteps.get(t).doubleValue());
            theta = Math.atan2(b2.getY() - b1.getY(), b2.getX() - b1.getX());
            lx = amp * Math.cos(theta);
            ly = amp * Math.sin(theta);
            bxp1 = b1.getX() + (b2.getX() - b1.getX()) * c;
            byp1 = b1.getY() + (b2.getY() - b1.getY()) * c;
            bxp2 = b1.getX() + (b2.getX() - b1.getX()) * (1 - c);
            byp2 = b1.getY() + (b2.getY() - b1.getY()) * (1 - c);
            at.setToIdentity();
            at.rotate(arot, bxp1, byp1);
            parallel.setLocation(bxp1 - lx, byp1 - ly);
            perp1.setLocation(0.d, 0.d);
            at.transform(parallel, perp1);
            at.setToIdentity();
            at.rotate(arot, bxp2, byp2);
            parallel.setLocation(bxp2 - lx, byp2 - ly);
            perp2.setLocation(0.d, 0.d);
            at.transform(parallel, perp2);
            gp.moveTo(b1.getX(), b1.getY());
            gp.curveTo(perp1.getX(), perp1.getY(), perp2.getX(), perp2.getY(), b2.getX(), b2.getY());
            arot = -arot;
            b1.setLocation(b2);
        }

        return gp;
    }

    // constructLatexString() almost duplicates drawingRoutine(), simplify?
    private String constructLatexString(final float scale, final int canvasHeight) {
        final StringBuffer command = new StringBuffer(128);

        final Point2D b1 = new Point2D.Double(getX(), getY());
        double bxp1, byp1, bxp2, byp2, lx, ly, theta;
        double arot = Math.PI / 2;
        final double c = 4.d / (3.d * Math.PI);
        final double amp =   (2.d / 3.d) * getAmp();
        final Point2D parallel = new Point2D.Double();
        final Point2D perp1 = new Point2D.Double();
        final Point2D perp2 = new Point2D.Double();

        final AffineTransform at = new AffineTransform();

        for (int t = 0; t < renormsteps.size(); t++) {
            tryNextBezierPoint(renormsteps.get(t).doubleValue());
            theta = Math.atan2(b2.getY() - b1.getY(), b2.getX() - b1.getX());
            lx = amp * Math.cos(theta);
            ly = amp * Math.sin(theta);
            bxp1 = b1.getX() + (b2.getX() - b1.getX()) * c;
            byp1 = b1.getY() + (b2.getY() - b1.getY()) * c;
            bxp2 = b1.getX() + (b2.getX() - b1.getX()) * (1 - c);
            byp2 = b1.getY() + (b2.getY() - b1.getY()) * (1 - c);
            at.setToIdentity();
            at.rotate(arot, bxp1, byp1);
            parallel.setLocation(bxp1 - lx, byp1 - ly);
            perp1.setLocation(0.d, 0.d);
            at.transform(parallel, perp1);
            at.setToIdentity();
            at.rotate(arot, bxp2, byp2);
            parallel.setLocation(bxp2 - lx, byp2 - ly);
            perp2.setLocation(0.d, 0.d);
            at.transform(parallel, perp2);
            final Point2D latexP1 = getLatexPoint(b1, scale, canvasHeight);
            final Point2D latexP2 = getLatexPoint(perp1, scale, canvasHeight);
            final Point2D latexP3 = getLatexPoint(perp2, scale, canvasHeight);
            final Point2D latexP4 = getLatexPoint(b2, scale, canvasHeight);
            command.append(' ').append(bezierLatexCommand("", latexP1, latexP2, latexP3, latexP4));
            arot = -arot;
            b1.setLocation(b2);
        }

        return command.toString();
    }

    private void initRenormSteps() {
        // Initialize the vector of renormalized timesteps
        renormsteps.clear();

        // First thing: calculate the (approximate) length of the curve.
        final double length = getBezierLength();

        final int wnumb = 2 * (int) Math.round(length * getFrequency());

        // Second: define the time step
        // and the tolerance for the last point according to frequency stretching;
        final double tstep = 1.f / wnumb;
        final double lptol = isNoFreqStretching() ? .015 : .0001;

        // Third: draw the Bezier
        final Point2D b1 = new Point2D.Double(getX(), getY());

        // First loop: fill
        // the vector of renormalized time steps to be used when drawing
        // the curve; if frequency stretching is allowed, the vector will
        // contain equal timesteps
        double tpar = tstep;
        while (tpar <= (1.d + tstep)) {
            tpar = setNextPoint(b1, tpar, tstep, this.isNoFreqStretching());

            if (Math.abs(tpar - 1 - tstep) < lptol
                    && ((Math.abs(b2.getX() - getX()) > .1)
                    || (Math.abs(b2.getY() - getY()) > .1))) {
                break;
            }
            b1.setLocation(b2);
            tpar += tstep;
        }

        // Overshooting will eventually occur only for the last point in
        // renormsteps; if this is the case remove the last element
        if (((renormsteps.get(renormsteps.size() - 1)).doubleValue() - 1.d) > .0000001) {
            renormsteps.remove(renormsteps.size() - 1);
        }

        // Calculate the correction to apply for each timestep, and
        // reset all the components of the renormalized timestep vector
        // in such a way that its last component will be 1.0
        final double corr = (1.d - (renormsteps.get(renormsteps.size() - 1)).doubleValue())
                / (renormsteps.size());

        for (int i = 0; i < renormsteps.size(); i++) {
            final double rvalue = (renormsteps.get(i)).doubleValue() + corr * (i + 1);
            renormsteps.set(i, Double.valueOf(rvalue));
        }
    }
}
