/*
 * jcurl java curling software framework http://www.jcurl.org Copyright (C)
 * 2005-2009 M. Rohrmoser
 * 
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
 * Place, Suite 330, Boston, MA 02111-1307 USA
 */

package org.jcurl.core.ui;

import java.awt.geom.Point2D;

import javax.swing.BoundedRangeModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import org.apache.commons.logging.Log;
import org.jcurl.core.api.ComputedTrajectorySet;
import org.jcurl.core.api.Curler;
import org.jcurl.core.api.CurveRock;
import org.jcurl.core.api.CurveStore;
import org.jcurl.core.api.IceSize;
import org.jcurl.core.api.Rock;
import org.jcurl.core.api.RockDouble;
import org.jcurl.core.api.RockSet;
import org.jcurl.core.api.RockType;
import org.jcurl.core.api.RockType.Pos;
import org.jcurl.core.api.RockType.Vel;
import org.jcurl.core.log.JCLoggerFactory;
import org.jcurl.math.CurveCombined;
import org.jcurl.math.MathVec;

/**
 * Push changes down from this {@link BroomPromptModel} to a
 * {@link ComputedTrajectorySet}.
 * <p>
 * Does <b>not sync back</b> events from the {@link ComputedTrajectorySet}!
 * </p>
 * 
 * @author <a href="mailto:m@jcurl.org">M. Rohrmoser </a>
 * @version $Id$
 */
public class TrajectoryBroomPromptWrapper extends DefaultBroomPromptModel
		implements ChangeListener {
	/**
	 * Mediates {@link ComputedTrajectorySet} and {@link BroomPromptModel} under
	 * a convenient, stateless, high-level interface.
	 * 
	 * @author <a href="mailto:m@jcurl.org">M. Rohrmoser </a>
	 * @version $Id$
	 */
	private static class MediatorWorker {
		private static final Log log = JCLoggerFactory
				.getLogger(MediatorWorker.class);

		private static void release(final Rock<Pos> r) {
			r.setLocation(0, IceSize.FAR_HACK_2_TEE, Math.PI);
		}

		private int findRunning(final RockSet<Vel> ivel, final int idx16,
				final boolean forceCheck) {
			if (ivel == null)
				return forceCheck ? -1 : idx16;
			if (forceCheck || idx16 < 0) {
				// Try to find the running rock.
				// 1st try: if there's only one rock running, that's it.
				int tmp = -1;
				for (int i = RockSet.ROCKS_PER_SET - 1; i >= 0; i--)
					if (ivel.getRock(i).isNotZero()) {
						if (!forceCheck)
							return i;
						if (tmp >= 0)
							throw new RuntimeException(
									"Multiple running rocks: " + tmp + i);
						tmp = i;
					}
				return tmp;
			} else
				return idx16;
		}

		private void startPos(final RockSet<Pos> pos, final int idx16) {
			if (pos == null || idx16 < 0)
				return;
			release(pos.getRock(idx16));
		}

		private <R extends RockType> void swap(final RockSet<R> r, final int a,
				final int b) {
			if (r == null || a == b || a < 0 || b < 0)
				return;
			final Rock<R> tmp = new RockDouble<R>(r.getRock(a));
			r.getRock(a).setLocation(r.getRock(b));
			r.getRock(b).setLocation(tmp);
		}

		/**
		 * Does <b>NOT</b> fire change events of the two {@link RockSet}s.
		 * 
		 * @param newPos
		 * @param splitTimeMillis
		 * @param idx16
		 * @param outTurn
		 * @param curl
		 * @param dstPos
		 * @param dstVel
		 */
		@SuppressWarnings("unchecked")
		private void syncBpm2Cts(final Point2D newPos,
				final long splitTimeMillis, final int idx16,
				final boolean outTurn, final Curler curl,
				final RockSet<Pos> dstPos, final RockSet<Vel> dstVel) {
			if (log.isDebugEnabled())
				log.debug("i=" + idx16);
			// set to initial pos
			final Rock start = new RockDouble(0, IceSize.FAR_HACK_2_TEE,
					Math.PI);
			dstPos.getRock(idx16).setLocation(start);

			// Compute direction
			MathVec.sub(newPos, start.p(), start.p());
			MathVec.norm(start.p(), start.p());

			// Compute initial Speed
			final double v0 = curl.computeHackSpeed(splitTimeMillis * 1e-3,
					newPos);
			MathVec.mult(v0, start.p(), start.p());

			// Handle
			start.setA((outTurn ? 1 : -1) * 0.25);
			dstVel.getRock(idx16).setLocation(start);
		}

		private void syncCts2Bpm(final int idx16, final RockSet<Pos> ipos,
				final RockSet<Vel> ivel, final Curler curl,
				final CurveCombined<CurveRock<Pos>> rc,
				final BroomPromptModel dst) {
			release(ipos.getRock(idx16));
			final Rock<Vel> v = ivel.getRock(idx16);

			// Compute Handle
			dst.setOutTurn(v.getA() >= 0);

			// Compute Broom Location
			final Point2D x = ipos.getRock(idx16).p();
			final double bY = dst.getBroom() == null ? 0 : dst.getBroom()
					.getY();
			final Point2D b2 = new Point2D.Double((bY - x.getY()) * v.getX()
					/ v.getY(), bY);
			if (log.isDebugEnabled())
				log.debug("v=" + v + ", x=" + b2);
			dst.setBroom(b2);

			// Compute Split Time
			dst.getSplitTimeMillis().setValue(
					(int) (1000 * curl.computeIntervalTime(rc.first())));
			if (dst.getIdx16() < 0)
				dst.setIdx16(idx16);
		}
	}

	private static final Log log = JCLoggerFactory
			.getLogger(TrajectoryBroomPromptWrapper.class);

	private ComputedTrajectorySet cts = null;
	private Curler curler = null;
	private CurveStore curveStore = null;
	private final MediatorWorker med = new MediatorWorker();
	private RockSet<Pos> positions = null;
	private RockSet<Vel> velocities = null;

	private void cts_resume() {
		if (cts != null)
			cts.setSuspended(false);
	}

	private void cts_suspend() {
		if (cts != null)
			cts.setSuspended(true);
	}

	public void init(final ComputedTrajectorySet cts) {
		this.cts = cts;
		if (cts == null)
			init(null, null, null, null);
		else
			init(cts.getInitialPos(), cts.getInitialVel(), cts.getCurler(), cts
					.getCurveStore());
	}

	private void init(final RockSet<Pos> positions,
			final RockSet<Vel> velocities, final Curler curler,
			final CurveStore curveStore) {
		cts_suspend();
		try {
			this.curveStore = curveStore;
			this.curler = curler;

			final int idx16;
			if (this.velocities != velocities)
				idx16 = med.findRunning(velocities, getIdx16(), true);
			else
				idx16 = getIdx16();
			// put rock idx16 into start position
			if (this.positions != positions)
				med.swap(positions, getIdx16(), idx16);
			med.startPos(positions, idx16);

			this.positions = positions;
			this.velocities = velocities;

			super.setIdx16(idx16);
			if (this.curveStore == null || this.curler == null
					|| this.positions == null || this.velocities == null) {
				setBroom(null);
				return;
			}
			updateBroom(true);
		} finally {
			cts_resume();
		}
	}

	@Override
	public void setBroom(final Point2D broom) {
		try {
			cts_suspend();
			// do the update no matter if the reference changed at all.
			super.setBroom(broom);
			updateVelocities();
		} finally {
			cts_resume();
		}
	}

	@Override
	public void setIdx16(final int idx16) {
		log.debug(idx16);
		final int old = getIdx16();
		if (old == idx16)
			return;
		try {
			cts_suspend();
			super.setIdx16(idx16);
			med.swap(positions, old, getIdx16());
			med.swap(velocities, old, getIdx16());
			updateVelocities();
		} finally {
			cts_resume();
		}
	}

	@Override
	public void setOutTurn(final boolean outTurn) {
		if (getOutTurn() == outTurn)
			return;
		try {
			cts_suspend();
			super.setOutTurn(outTurn);
			updateVelocities();
		} finally {
			cts_resume();
		}
	}

	@Override
	public void setSplitTimeMillis(final BoundedRangeModel splitTimeMillis) {
		if (getSplitTimeMillis() == splitTimeMillis)
			return;
		try {
			cts_suspend();
			if (getSplitTimeMillis() != null)
				getSplitTimeMillis().removeChangeListener(this);
			super.setSplitTimeMillis(splitTimeMillis);
			if (getSplitTimeMillis() != null)
				getSplitTimeMillis().addChangeListener(this);
			updateVelocities();
		} finally {
			cts_resume();
		}
	}

	@Override
	public void setValueIsAdjusting(final boolean valueIsAdjusting) {
		super.setValueIsAdjusting(valueIsAdjusting);
	}

	public void stateChanged(final ChangeEvent evt) {
		if (evt.getSource() == getSplitTimeMillis())
			updateVelocities();
		else
			log.warn("Unprocessed event " + evt);
	}

	@SuppressWarnings("unchecked")
	private void updateBroom(final boolean isInit) {
		cts_suspend();
		try {
			// syncCts2Bpm calls back setOutTurn which leads to a buggy cyclic
			// computation. To prevent this we use a (dirty) trick:
			final RockSet<Vel> ivel = velocities;
			try {
				if (isInit)
					velocities = null;
				med.syncCts2Bpm(getIdx16(), positions, ivel, curler,
						(CurveCombined<CurveRock<Pos>>) curveStore
								.getCurve(getIdx16()), this);
			} finally {
				velocities = ivel;
			}
		} finally {
			cts_resume();
		}
	}

	/**
	 * Call
	 * {@link MediatorWorker#syncBpm2Cts(Point2D, long, int, boolean, Curler, RockSet, RockSet)}
	 * and {@link RockSet#fireStateChanged()}. Maybe the latter should be
	 * replaced by {@link Rock#fireStateChanged} (part of
	 * {@link Rock#setLocation(double, double, double)}.
	 */
	private void updateVelocities() {
		if (positions == null || velocities == null || curler == null
				|| getBroom() == null || getSplitTimeMillis() == null)
			return;
		med.syncBpm2Cts(getBroom(), getSplitTimeMillis().getValue(),
				getIdx16(), getOutTurn(), curler, positions, velocities);
		// velocities.fireStateChanged();
	}
}
