/* Copyright (c) 2011, Geert Bergman (geert@scrivo.nl)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of "Scrivo" nor the names of its contributors may be
 *    used to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 * $Id: HSVSelector.js 616 2013-04-22 23:48:38Z geert $
 */

"use strict";

SUI.control.HSVSelector = SUI.defineClass(
	/** @lends SUI.control.HSVSelector.prototype */{

	/** @ignore */ baseClass: SUI.Box,

	/**
	 * @class
	 * SUI.control.HSVSelector is a hue, saturation, value color selection
	 * control as can be found in image manipulation programs as the GIMP.
	 * The user can make a color section by choosing the hue value from a bar
	 * and the saturation and value from a cartesian system. The hue bar is
	 * representing the perimeter of a color wheel cycling through the red,
	 * green and blue colors while blending into each other). The saturation
	 * and value are selected by selecting a point in an area that is set up
	 * by these two axes.
	 *
	 * @augments SUI.Box
	 *
	 * @description
	 * Create an HSV color selection control.
	 *
	 * @constructs
	 * @param see base class
	 * @param {String} arg.color Initial color selection of the control.
	 * @param {Function} arg.onChange Listener function that is executed each
	 *     time the control's color selection changes.
	 */
	initializer: function(arg) {

		SUI.control.HSVSelector.initializeBase(this, arg);

		// set the width and height of the control. this is a fixed size
		// which can't be altered.
		this.width(2 * this.PADDING + this.AXIS_LENGTH + this.SPLIT_WIDTH
		 + this.HUE_BAR_WIDTH);
		this.height(this.AXIS_LENGTH + 2 * this.PADDING);

		// conversion from length to range can be done nicely by substracting
		// the crosshair width of the length
		this._axisRange = this.AXIS_LENGTH - this.CROSSHAIR_LINE;

		// built the saturation-value pane ...
		this._buildSatValPane();
		// ... and hue bar
		this._buildHueBar();

		// set the onChange handler
		if (arg.onChange) {
		 this.addListener("onChange", arg.onChange);
		}

		// set the control's selected color value
		this.colorCode(arg.color || "#CCCCCC");
	},

	/**
	 * Length of the hue, saturation and value axes.
	 */
	AXIS_LENGTH: 128,

	/**
	 * Width of the crosshair line.
	 */
	CROSSHAIR_LINE: 1,

	/**
	 * Width of the hue bar.
	 */
	HUE_BAR_WIDTH: 20,

	/**
	 * Height of the hue crosshair image.
	 */
	HUE_CROSSHAIR_HEIGHT: 15,

	/**
	 * Width of the hue crosshair image.
	 */
	HUE_CROSSHAIR_WIDTH: 26,

	/**
	 * Padding of the control (half of the sat.-value crosshair image size).
	 */
	PADDING: 15,

	/**
	 * Width and height of the saturation-value crosshair image
	 */
	SATVAL_CROSSHAIR_SIZE: 31,

	/**
	 * distance between the saturation-value pane and hue bar
	 */
	SPLIT_WIDTH: 17,

	/**
	 * Set or get the HTML color code selection of the control.
	 * @param {String} c An HTML color code (#FF7700), or none to get
	 *    the current color selection from the control.
	 * @return {String} An HTML color code (#FF7700), or null if this
	 *    method is used as a setter
	 */
	colorCode: function(c) {
	 if (c === undefined) {
			return SUI.color.hsvToCol(
				 {h: this._hue, s: this._sat, v: this._val});
	 }
	 // got here? the method is a setter
		var col = SUI.color.colToHsv(c);
		// set the new color values
		this._hue = col.h;
		this._sat = col.s;
		this._val = col.v;
		// set the positions of the crosshairs
		this._hueTop();
		this._satTop();
		this._valLeft();
		// redisplay the control
		this.display();

		return null;
	},

	/**
	 * Display HSV control. Set the size and position of the
	 * saturation-value pane, hue bar, crosshairs and set the background
	 * color of the saturation-value.
	 */
	display: function() {
		this.setDim();
		this._satVal.setDim();
		this._hueBar.setDim();
		this._chSatVal.setDim();
		this._chHue.setDim();
		this._satVal.el().style.backgroundColor =
			SUI.color.hsvToCol({h: this._hue, s: 1, v:1});
	},

	/**
	 * Set or get the hue of the control's currently selected color and
	 * redisplay the control if the value was set.
	 * @param {int} hue The new hue for the control's currently selected color
	 *     (0 <= hue <= 360), or no value to use this method as a getter.
	 * @return {int} the hue of the control's currently selected color, or
	 *     null if this method is used as a setter.
	 */
	hue: function(hue) {
	 if (hue === undefined) {
	   return this._hue;
	 }
	 // got here? the method is a setter
		if (hue >= 0 && hue <= 360) {
			this._hue = hue;
		}
		this._hueTop();
		this.display();

	 return null;
	},

	/**
	 * Lay out the HSV control. Calculate the size and position of the
	 * saturation-value pane and hue bar.
	 */
	layOut: function() {
		this._satVal.setRect(this._satVal);
		this._hueBar.setRect(this._hueBar);
	},

	/**
	 * onChange event handler: is executed when the control's color selection
	 * changes. This happens continuously when the user is dragging the
	 * crosshairs.
	 * @param {String} c The HTML color code of the color that is currently
	 *     selected by the control.
	 */
	onChange: function(c) {
	},

	/**
	 * Set the saturation of the control's currently selected color and
	 * redisplay the control.
	 * @param {float} sat The new saturation for the control's currently
	 *     selected color (0 <= saturation <= 1), or no value to use this
	 *     method as a getter.
	 * @return {float} the saturation of the control's currently selected
	 *     color, or null if this method is used as a setter.
	 */
	saturation: function(sat) {
	 if (sat === undefined) {
	   return this._sat;
	 }
	 // got here? the method is a setter
		if (sat >= 0 && sat <= 1) {
			this._sat = sat;
		}
		this._satTop();
		this.display();

	 return null;
	},

	/**
	 * Set the value of the control's currently selected color and redisplay
	 * the control.
	 * @param {float} val The new value for the control's currently selected
	 *     color (0 <= value <= 1), or no value to use this method as a
	 *     getter.
	 * @return {float} the value of the control's currently selected
	 *     color, or null if this method is used as a setter.
	 */
	value: function(val) {
	 if (val === undefined) {
	   return this._val;
	 }
	 // got here? the method is a setter
		if (val >= 0 && val <= 1) {
			this._val = val;
		}
		this._valLeft();
		this.display();

	 return null;
	},

	// length vs range: fi length: 128 -> range: 0-127
	_axisRange: 0,

	// dragger box for the hue crosshair
	_chHue: null,

	// dragger box for the saturation-value pane crosshair
	_chSatVal: null,

	// current hue setting
	_hue: 0,

	// box for the hue bar
	_hueBar: null,

	// offset of the hue crosshair w.r.t. the top of the hue bar
	_ofsChHue: 0,

	// offset of the saturation-value crosshair w.r.t. the top left corner
	// of the saturation-value pane
	_ofsChSatVal: 0,

	// current saturation setting
	_sat: 1,

	// box for the saturation-value pane
	_satVal: null,

	// current value setting
	_val: 0,

	// Create the box for the hue bar and create the draggable crosshair
	_buildHueBar: function() {

	 // build the hue bar
		this._hueBar = new SUI.Box({
		 parent: this,
			width: this.HUE_BAR_WIDTH,
			height: this.AXIS_LENGTH,
			top: this.PADDING,
			left: this.AXIS_LENGTH + this.PADDING + this.SPLIT_WIDTH
		});
		// set hue background image
		this._hueBar.el().style.backgroundImage = "url(" + SUI.imgDir + "/"
			+ SUI.resource.hsvHue + ")";
		// the crosshair extends over the hue bar's edges, show it
		this._hueBar.el().style.overflow = "visible";

		// create the crosshair, a dragger component
		this._chHue = new SUI.Dragger({
		 parent: this._hueBar,
		 width: this.HUE_CROSSHAIR_WIDTH,
		 height: this.HUE_CROSSHAIR_HEIGHT,
			left: (this.HUE_BAR_WIDTH - this.HUE_CROSSHAIR_WIDTH) / 2 | 0
		});
		// calculate the top offset of the crosshair image w.r.t. the top of
		// the hue bar
		this._ofsChHue =
		 (this.CROSSHAIR_LINE - this.HUE_CROSSHAIR_HEIGHT) / 2 | 0;
		// set the range for dragging ...
		this._chHue.yMin(this._ofsChHue);
		this._chHue.yMax(
		 this._ofsChHue + this.AXIS_LENGTH - this.CROSSHAIR_LINE);
		// ... and the direction in which we may draw
		this._chHue.direction(this._chSatVal.VERTICAL);
		// set the crosshair background image ...
		this._chHue.el().style.backgroundImage =
		 "url(" + SUI.imgDir + "/" + SUI.resource.hsvChHue + ")";
		// ... and an appropriate cursor
		this._chHue.el().style.cursor = "pointer";

		// start dragging on the onmousedown of the crosshair
		var that = this;
		SUI.browser.addEventListener(this._chHue.el(), "mousedown",
		 function(e) {
			 if (!that._startHueDrag(new SUI.Event(this, e))) {
			   SUI.browser.noPropagation(e);
			 }
		 }
		);

	},

	// Create the box for the saturation-value pane and create the draggable
	// crosshair
	_buildSatValPane: function() {

	 // build the saturation-value pane
		this._satVal = new SUI.Box({
		 parent: this,
			width: this.AXIS_LENGTH,
		 height: this.AXIS_LENGTH,
		 top: this.PADDING,
		 left: this.PADDING
		});
		// set saturation-value background image
		this._satVal.el().style.backgroundImage = "url(" + SUI.imgDir + "/"
			+ SUI.resource.hsvSatVal + ")";
		// the crosshair extends over the pane's edges, show it
		this._satVal.el().style.overflow = "visible";

		// create the crosshair, a dragger component
		this._chSatVal = new SUI.Dragger({
		 parent: this._satVal,
		 width: this.SATVAL_CROSSHAIR_SIZE,
		 height: this.SATVAL_CROSSHAIR_SIZE
		});
		// calculate the top offset of the crosshair image w.r.t. the top left
		// of the saturation-value pane
		this._ofsChSatVal =
		 (this.CROSSHAIR_LINE - this.SATVAL_CROSSHAIR_SIZE) / 2 | 0;
		// set the range for dragging ...
		var max = this._ofsChSatVal + this.AXIS_LENGTH - this.CROSSHAIR_LINE;
		this._chSatVal.xMin(this._ofsChSatVal);
		this._chSatVal.xMax(max);
		this._chSatVal.yMin(this._ofsChSatVal);
		this._chSatVal.yMax(max);
		// ... and the direction in which we may draw
		this._chSatVal.direction(
		 this._chSatVal.HORIZONTAL + this._chSatVal.VERTICAL);
		// set the crosshair background image ...
		this._chSatVal.el().style.backgroundImage =
		 "url(" + SUI.imgDir + "/" + SUI.resource.hsvChSatVal + ")";
		// ... and an appropriate cursor
		this._chSatVal.el().style.cursor = "pointer";

		// start dragging on the onmousedown of the crosshair
		var that = this;
		SUI.browser.addEventListener(this._chSatVal.el(), "mousedown",
		 function(e) {
			 if (!that._startSatValDrag(new SUI.Event(this, e))) {
			   SUI.browser.noPropagation(e);
			 }
		 }
		);

	},

	// While dragging the hue crosshair change the hue setting and update
	// the hue setting of the saturation-value pane and call the onChange
	// listener
	_hueDrag: function() {
	 // calculate hue from top (hue = distance / range * 360)
		this._hue = (this._axisRange - (this._chHue.top() - this._ofsChHue))
		 * 360 / this._axisRange;
	 // set the background color of the saturation-value pane
	 this._satVal.el().style.backgroundColor =
			SUI.color.hsvToCol({h: this._hue, s:1, v:1});
	 // call the listener
		this.callListener("onChange",
			SUI.color.hsvToCol({h: this._hue, s: this._sat, v: this._val}));
	},

	// Set the top of the hue crosshair to reflect the current hue setting.
	_hueTop: function() {
		// calculate top from hue (distance = (hue / 360) * range)
		this._chHue.top(
		 ((1 - this._hue / 360) * this._axisRange | 0) + this._ofsChHue);
	},

	// Set the top of the saturation-value crosshair to reflect the current
	// saturation setting.
	_satTop: function() {
		// calculate top from saturation (distance = saturation * range)
		this._chSatVal.top(
		 ((1 - this._sat) * this._axisRange | 0) + this._ofsChSatVal);
	},

	// While dragging the saturation crosshair change the saturation and
	// value settings and call the onChange listener
	_satValDrag: function() {
		// calculate saturation from top (saturation = distance / range)
		this._sat =
				(this._axisRange - (this._chSatVal.top() - this._ofsChSatVal))
	   / this._axisRange;
		// calculate value from left (value = distance / range)
		this._val =
		 (this._chSatVal.left() - this._ofsChSatVal) / this._axisRange;
		// call the listener
		this.callListener("onChange",
		   SUI.color.hsvToCol({h: this._hue, s: this._sat, v: this._val}));
	},

	// On start dragging the hue crosshair initialize the onDrag handler of
	// the dragger and start dragging
	_startHueDrag: function(e) {
		var that = this;
		this._chHue.addListener("onDrag", function() { that._hueDrag(); });
		this._chHue.start(e, this);
	},

	// On start dragging the saturation-value crosshair initialize the
	// onDrag handler of the dragger and start dragging
	_startSatValDrag: function(e) {
		var that = this;
		this._chSatVal.addListener(
		 "onDrag", function() { that._satValDrag(); });
		this._chSatVal.start(e, this);
	},

	// Set the left of the saturation-value crosshair to reflect the current
	// value setting.
	_valLeft: function() {
		// calculate left from value (distance = value * range)
		this._chSatVal.left(
		 (this._val * this._axisRange | 0) + this._ofsChSatVal);
	}

});