package controlP5;

/**
 * controlP5 is a processing gui library.
 *
 *  2006-2012 by Andreas Schlegel
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1
 * of the License, or (at your option) any later version.
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General
 * Public License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA 02111-1307 USA
 *
 * @author 		Andreas Schlegel (http://www.sojamo.de)
 * @modified	08/27/2012
 * @version		0.7.6
 *
 */

import processing.core.PApplet;
import processing.core.PVector;

/**
 * A knob is a circular slider which can be used with a limited and unlimited range. Knobs come in 3
 * designs LINE, ARC and ELIPSE and can be controller with both the mouse and the mouse wheel.
 * 
 * @example controllers/ControlP5knob
 */
public class Knob extends Controller<Knob> {

	protected float _myDiameter;

	protected float _myRadius;

	protected float myAngle;

	protected float startAngle;

	protected float angleRange;

	protected float resolution = 200.0f; // sensitivity.

	protected int _myTickMarksNum = 8;

	protected boolean isShowTickMarks;

	protected boolean isSnapToTickMarks;

	protected int myTickMarkLength = 2;

	protected float myTickMarkWeight = 1;

	protected boolean isShowAngleRange = true;

	protected float currentValue;

	protected float previousValue;

	protected float modifiedValue;

	protected boolean isConstrained;

	protected int _myDragDirection = HORIZONTAL;

	protected int viewStyle = LINE;

	public static int autoWidth = 39;

	public static int autoHeight = 39;

	protected PVector autoSpacing = new PVector(10, 20, 0);

	private float scrollSensitivity = 1.0f / resolution;

	/**
	 * Convenience constructor to extend Knob.
	 * 
	 * @example use/ControlP5extendController
	 * @param theControlP5
	 * @param theName
	 */
	public Knob(ControlP5 theControlP5, String theName) {
		this(theControlP5, theControlP5.getDefaultTab(), theName, 0, 100, 0, 0, 0, autoWidth);
		theControlP5.register(theControlP5.papplet, theName, this);
	}

	/**
	 * @exclude
	 */
	public Knob(ControlP5 theControlP5, ControllerGroup<?> theParent, String theName, float theMin, float theMax, float theDefaultValue, int theX, int theY, int theWidth) {
		super(theControlP5, theParent, theName, theX, theY, theWidth, theWidth);
		_myValue = theDefaultValue;
		setMin(theMin);
		setMax(theMax);
		_myDiameter = theWidth;
		_myRadius = _myDiameter / 2;
		_myUnit = (_myMax - _myMin) / ControlP5Constants.TWO_PI;
		startAngle = HALF_PI + PI * 0.25f;
		angleRange = PI + HALF_PI;
		myAngle = startAngle;
		isConstrained = true;
		getCaptionLabel().align(CENTER, BOTTOM_OUTSIDE);
	}

	public Knob setSize(float theWidth, float theHeight) {
		return setRadius(theWidth / 2);
	}

	public Knob setRadius(float theValue) {
		_myRadius = theValue;
		_myDiameter = _myRadius * 2;
		width = (int) _myDiameter;
		height = (int) _myDiameter;
		return this;
	}

	public float getRadius() {
		return _myRadius;
	}

	/**
	 * The start angle is a value between 0 and TWO_PI. By default the start angle is set to HALF_PI
	 * + PI * 0.25f
	 */
	public Knob setStartAngle(float theAngle) {
		startAngle = theAngle;
		setInternalValue(modifiedValue);
		return this;
	}

	/**
	 * get the start angle, 0 is at 3 o'clock.
	 */
	public float getStartAngle() {
		return startAngle;
	}

	/**
	 * set the range in between which the know operates. By default the range is PI + HALF_PI
	 */
	public Knob setAngleRange(float theRange) {
		angleRange = theRange;
		setInternalValue(modifiedValue);
		return this;
	}

	public float getAngleRange() {
		return angleRange;
	}

	public float getAngle() {
		return myAngle;
	}

	public boolean isShowAngleRange() {
		return isShowAngleRange;
	}

	public Knob setShowAngleRange(boolean theValue) {
		isShowAngleRange = theValue;
		return this;
	}

	/**
	 * Sets the drag direction, when controlling a knob, parameter is either Controller.HORIZONTAL
	 * or Controller.VERTICAL.
	 * 
	 * @param theValue must be Controller.HORIZONTAL or Controller.VERTICAL
	 * @return Knob
	 */
	public Knob setDragDirection(int theValue) {
		if (theValue == HORIZONTAL) {
			_myDragDirection = HORIZONTAL;
		} else {
			_myDragDirection = VERTICAL;
		}
		return this;
	}

	/**
	 * Gets the drag direction which is either Controller.HORIZONTAL or Controller.VERTICAL.
	 * 
	 * @return int returns Controller.HORIZONTAL or Controller.VERTICAL
	 */
	public int getDragDirection() {
		return _myDragDirection;
	}

	/**
	 * resolution is a sensitivity value when dragging a knob. the higher the value, the more
	 * sensitive the dragging.
	 */
	public Knob setResolution(float theValue) {
		resolution = theValue;
		return this;
	}

	public float getResolution() {
		return resolution;
	}

	public Knob setNumberOfTickMarks(int theNumber) {
		_myTickMarksNum = theNumber;
		showTickMarks();
		return this;
	}

	public int getNumberOfTickMarks() {
		return _myTickMarksNum;
	}

	public Knob showTickMarks() {
		isShowTickMarks = true;
		return this;
	}

	public Knob hideTickMarks() {
		isShowTickMarks = false;
		return this;
	}

	public boolean isShowTickMarks() {
		return isShowTickMarks;
	}

	public Knob snapToTickMarks(boolean theFlag) {
		isSnapToTickMarks = theFlag;
		update();
		return this;
	}

	public Knob setTickMarkLength(int theLength) {
		myTickMarkLength = theLength;
		return this;
	}

	public int getTickMarkLength() {
		return myTickMarkLength;
	}

	public Knob setTickMarkWeight(float theWeight) {
		myTickMarkWeight = theWeight;
		return this;
	}

	public float getTickMarkWeight() {
		return myTickMarkWeight;
	}

	public Knob setConstrained(boolean theValue) {
		isConstrained = theValue;
		if (!isConstrained) {
			setShowAngleRange(false);
		} else {
			setShowAngleRange(true);
		}
		return this;
	}

	public boolean isConstrained() {
		return isConstrained;
	}

	/**
	 * @exclude
	 */
	@Override @ControlP5.Invisible public Knob updateInternalEvents(PApplet theApplet) {
		if (isMousePressed && !cp5.keyHandler.isAltDown()) {
			if (isActive) {
				float c = (_myDragDirection == HORIZONTAL) ? _myControlWindow.mouseX - _myControlWindow.pmouseX : _myControlWindow.mouseY - _myControlWindow.pmouseY;
				currentValue += (c) / resolution;
				if (isConstrained) {
					currentValue = PApplet.constrain(currentValue, 0, 1);
				}
				setInternalValue(currentValue);
			}
		}
		return this;
	}

	protected void onEnter() {
		isActive = true;
	}

	protected void onLeave() {
		isActive = false;
	}

	/**
	 * @exclude {@inheritDoc}
	 */
	@Override @ControlP5.Invisible public void mousePressed() {
		float x = _myParent.getAbsolutePosition().x + position.x + _myRadius;
		float y = _myParent.getAbsolutePosition().y + position.y + _myRadius;
		if (PApplet.dist(x, y, _myControlWindow.mouseX, _myControlWindow.mouseY) < _myRadius) {
			isActive = true;
			if (PApplet.dist(x, y, _myControlWindow.mouseX, _myControlWindow.mouseY) > (_myRadius * 0.6)) {
				myAngle = (PApplet.atan2(_myControlWindow.mouseY - y, _myControlWindow.mouseX - x) - startAngle);
				if (myAngle < 0) {
					myAngle = TWO_PI + myAngle;
				}
				if (isConstrained) {
					myAngle %= TWO_PI;
				}
				currentValue = PApplet.map(myAngle, 0, angleRange, 0, 1);
				setInternalValue(currentValue);
			}
		}
	}

	/**
	 * @exclude {@inheritDoc}
	 */
	@Override @ControlP5.Invisible public void mouseReleasedOutside() {
		isActive = false;
	}

	@Override public Knob setMin(float theValue) {
		_myMin = theValue;
		return this;
	}

	@Override public Knob setMax(float theValue) {
		_myMax = theValue;
		return this;
	}

	public Knob setRange(float theMin, float theMax) {
		setMin(theMin);
		setMax(theMax);
		update();
		return this;
	}

	protected void setInternalValue(float theValue) {
		modifiedValue = (isSnapToTickMarks) ? PApplet.round((theValue * _myTickMarksNum)) / ((float) _myTickMarksNum) : theValue;
		currentValue = theValue;
		myAngle = PApplet.map(isSnapToTickMarks == true ? modifiedValue : currentValue, 0, 1, startAngle, startAngle + angleRange);

		if (isSnapToTickMarks) {
			if (previousValue != modifiedValue && isSnapToTickMarks) {
				broadcast(FLOAT);
				previousValue = modifiedValue;
				return;
			}
		}
		if (previousValue != currentValue) {
			broadcast(FLOAT);
			previousValue = modifiedValue;
		}
	}

	@Override public Knob setValue(float theValue) {
		theValue = PApplet.map(theValue, _myMin, _myMax, 0, 1);
		if (isConstrained) {
			theValue = PApplet.constrain(theValue, 0, 1);
		}
		setInternalValue(theValue);
		return this;
	}

	@Override public float getValue() {
		_myValue = PApplet.map(_myTickMarksNum > 0 ? modifiedValue : currentValue, 0, 1, _myMin, _myMax);
		return _myValue;
	}

	/**
	 * Assigns a random value to the controller.
	 */
	public Knob shuffle() {
		float r = (float) Math.random();
		setValue(PApplet.map(r, 0, 1, getMin(), getMax()));
		return this;
	}

	/**
	 * Sets the sensitivity for the scroll behavior when using the mouse wheel or the scroll
	 * function of a multi-touch track pad. The smaller the value (closer to 0) the higher the
	 * sensitivity.
	 * 
	 * @param theValue
	 * @return Knob
	 */
	public Knob setScrollSensitivity(float theValue) {
		scrollSensitivity = theValue;
		return this;
	}

	/**
	 * Changes the value of the knob when hovering and using the mouse wheel or the scroll function
	 * of a multi-touch track pad.
	 */
	@ControlP5.Invisible public Knob scrolled(int theRotationValue) {
		float f = getValue();
		float steps = isSnapToTickMarks ? (1.0f / getNumberOfTickMarks()) : scrollSensitivity;
		f += (getMax() - getMin()) * (-theRotationValue * steps);
		setValue(f);
		return this;
	}

	/**
	 * @exclude
	 */
	@Override @ControlP5.Invisible public Knob update() {
		setValue(_myValue);
		return this;
	}

	/**
	 * set the display style of a knob. takes parameters Knob.LINE, Knob.ELLIPSE or Knob.ARC.
	 * default style is Knob.LINE
	 * 
	 * @param theStyle use Knob.LINE, Knob.ELLIPSE or Knob.ARC
	 * @return Knob
	 */
	public Knob setViewStyle(int theStyle) {
		viewStyle = theStyle;
		return this;
	}

	public int getViewStyle() {
		return viewStyle;
	}

	/**
	 * @exclude {@inheritDoc}
	 */
	@Override @ControlP5.Invisible public Knob updateDisplayMode(int theMode) {
		_myDisplayMode = theMode;
		switch (theMode) {
		case (DEFAULT):
			_myControllerView = new KnobView();
			break;
		case (SPRITE):
		case (IMAGE):
			_myControllerView = new KnobView();
			break;
		case (CUSTOM):
		default:
			break;
		}
		return this;
	}

	class KnobView implements ControllerView<Knob> {
		public void display(PApplet theApplet, Knob theController) {
			theApplet.translate(getRadius(), getRadius());

			theApplet.pushMatrix();
			theApplet.ellipseMode(PApplet.CENTER);
			theApplet.noStroke();
			theApplet.fill(getColor().getBackground());
			theApplet.ellipse(0, 0, getRadius() * 2, getRadius() * 2);
			theApplet.popMatrix();
			int c = isActive() ? getColor().getActive() : getColor().getForeground();
			theApplet.pushMatrix();
			if (getViewStyle() == Controller.LINE) {
				theApplet.rotate(getAngle());
				theApplet.stroke(c);
				theApplet.strokeWeight(getTickMarkWeight());
				theApplet.line(0, 0, getRadius(), 0);
			} else if (getViewStyle() == Controller.ELLIPSE) {
				theApplet.rotate(getAngle());
				theApplet.fill(c);
				theApplet.ellipse(getRadius() * 0.75f, 0, getRadius() * 0.2f, getRadius() * 0.2f);
			} else if (getViewStyle() == Controller.ARC) {
				theApplet.fill(c);
				theApplet.arc(0, 0, getRadius() * 1.8f, getRadius() * 1.8f, getStartAngle(), getAngle() + ((getStartAngle() == getAngle()) ? 0.06f : 0f));
				theApplet.fill(getColor().getBackground());
				theApplet.ellipse(0, 0, getRadius() * 1.2f, getRadius() * 1.2f);
			}
			theApplet.popMatrix();

			theApplet.pushMatrix();
			theApplet.rotate(getStartAngle());

			if (isShowTickMarks()) {
				float step = getAngleRange() / getNumberOfTickMarks();
				theApplet.stroke(getColor().getForeground());
				theApplet.strokeWeight(getTickMarkWeight());
				for (int i = 0; i <= getNumberOfTickMarks(); i++) {
					theApplet.line(getRadius() + 2, 0, getRadius() + getTickMarkLength() + 2, 0);
					theApplet.rotate(step);
				}
			} else {
				if (isShowAngleRange()) {
					theApplet.stroke(getColor().getForeground());
					theApplet.strokeWeight(getTickMarkWeight());
					theApplet.line(getRadius() + 2, 0, getRadius() + getTickMarkLength() + 2, 0);
					theApplet.rotate(getAngleRange());
					theApplet.line(getRadius() + 2, 0, getRadius() + getTickMarkLength() + 2, 0);
				}
			}
			theApplet.noStroke();
			theApplet.popMatrix();
			if (isLabelVisible) {
				_myCaptionLabel.draw(theApplet, -getWidth() / 2, -getHeight() / 2, theController);
			}
		}
	}

	/**
	 * @exclude
	 * @deprecated
	 */
	@Deprecated public Knob setOffsetAngle(float theValue) {
		return setStartAngle(theValue);
	}

	/**
	 * @exclude
	 * @deprecated
	 */
	@Deprecated public float value() {
		return getValue();
	}

	/**
	 * @exclude
	 * @deprecated
	 */
	@Deprecated public Knob setDisplayStyle(int theStyle) {
		viewStyle = theStyle;
		return this;
	}

	/**
	 * @exclude
	 * @deprecated
	 */
	@Deprecated public int getDisplayStyle() {
		return viewStyle;
	}

	/**
	 * @exclude
	 * @deprecated
	 */
	@Deprecated @ControlP5.Invisible public Knob setSensitivity(float theValue) {
		scrollSensitivity = theValue;
		return this;
	}

	/**
	 * @exclude
	 * @deprecated
	 */
	@Deprecated public Knob showTickMarks(boolean theFlag) {
		isShowTickMarks = theFlag;
		return this;
	}

}
/*
 * 
 * settings for:
 * 
 * TODO tickmarks: distance from edge
 * 
 * TODO only start-end marks if isLimited and tickmarks are off.
 * 
 * TODO arc: add setter for distance to center + distance to edge currently percental.
 * 
 * TODO enable/disable drag and click control (for endless, click should be disabled).
 * 
 * TODO dragging: add another option to control the knob. currently only linear dragging is
 * implemented, add circular dragging (as before) as well
 */

/*
 * (non-Javadoc)
 * 
 * @see controlP5.Controller#updateInternalEvents(processing.core.PApplet)
 */
