/**
	_enyo.Slideable_ is a control that can be dragged either horizontally or
	vertically between a minimum and a maximum value. When released from
	dragging, a	Slideable will animate to its minimum or maximum position,
	depending on the direction of the drag.

	The _min_ value specifies a position to the left of, or above, the initial
	position, to which the Slideable may be dragged.
	The _max_ value specifies a position to the right of, or below, the initial
	position, to which the Slideable may be dragged.
	The _value_ property specifies the current position of the Slideable,
	between the minimum and maximum positions.

	_min_, _max_, and _value_ may be specified in units of "px" or "%".

	The _axis_ property determines whether the Slideable slides left-to-right
	("h") or up-and-down ("v").

	The following control is placed 90% off the screen to the right, and slides
	to its natural position.

		{kind: "enyo.Slideable", value: -90, min: -90, unit: "%",
			classes: "enyo-fit", style: "width: 300px;",
			components: [
				{content: "stuff"}
			]
		}
*/
enyo.kind({
	name: "enyo.Slideable",
	kind: "Control",
	published: {
		//* Direction of sliding; can be "h" or "v"
		axis: "h",
		//* Current position of the Slideable (a value between _min_ and _max_)
		value: 0,
		//* Unit for _min_, _max_, and _value_; can be "px" or "%"
		unit: "px",
		//* A minimum value to slide to
		min: 0,
		//* A maximum value to slide to
		max: 0,
		//* When truthy, apply CSS styles to allow GPU compositing of slideable content
		//* if the platform allows.
		accelerated: "auto",
		//* Set to false to prevent the Slideable from dragging with elasticity
		//* past its _min_ or _max_ value
		overMoving: true,
		//* Set to false to disable dragging
		draggable: true
	},
	events: {
		//* Fires when the Slideable finishes animating.
		onAnimateFinish: "",
		//* Fires when the position (i.e., _value_) of the Slideable changes.
		onChange: ""
	},
	//* Set to true to prevent a drag from bubbling beyond the Slideable
	preventDragPropagation: false,
	//* @protected
	tools: [
		{kind: "Animator", onStep: "animatorStep", onEnd: "animatorComplete"}
	],
	handlers: {
		ondragstart: "dragstart",
		ondrag: "drag",
		ondragfinish: "dragfinish"
	},
	kDragScalar: 1,
	dragEventProp: "dx",
	unitModifier: false,
	canTransform: false,
	//* @protected
	create: function() {
		this.inherited(arguments);
		this.acceleratedChanged();
		this.transformChanged();
		this.axisChanged();
		this.valueChanged();
		this.addClass("enyo-slideable");
	},
	initComponents: function() {
		this.createComponents(this.tools);
		this.inherited(arguments);
	},
	rendered: function() {
		this.inherited(arguments);
		this.canModifyUnit();
		this.updateDragScalar();
	},
	resizeHandler: function() {
		this.inherited(arguments);
		this.updateDragScalar();
	},
	canModifyUnit: function() {
		if (!this.canTransform) {
			var b = this.getInitialStyleValue(this.hasNode(), this.boundary);
			// If inline style of "px" exists, while unit is "%"
			if (b.match(/px/i) && (this.unit === "%")) {
				// Set unitModifier - used to over-ride "%"
				this.unitModifier = this.getBounds()[this.dimension];
			}
		}
	},
	getInitialStyleValue: function(inNode, inBoundary) {
		var s = enyo.dom.getComputedStyle(inNode);
		if (s) {
			return s.getPropertyValue(inBoundary);
		} else if (inNode && inNode.currentStyle) {
			return inNode.currentStyle[inBoundary];
		}
		return "0";
	},
	updateBounds: function(inValue, inDimensions) {
		var inBounds = {};
		inBounds[this.boundary] = inValue;
		this.setBounds(inBounds, this.unit);

		this.setInlineStyles(inValue, inDimensions);
	},
	updateDragScalar: function() {
		if (this.unit == "%") {
			var d = this.getBounds()[this.dimension];
			this.kDragScalar = d ? 100 / d : 1;

			if (!this.canTransform) {
				this.updateBounds(this.value, 100);
			}
		}
	},
	transformChanged: function() {
		this.canTransform = enyo.dom.canTransform();
	},
	acceleratedChanged: function() {
		if (!enyo.platform.android || enyo.platform.android <= 2) {
			enyo.dom.accelerate(this, this.accelerated);
		}
	},
	axisChanged: function() {
		var h = this.axis == "h";
		this.dragMoveProp = h ? "dx" : "dy";
		this.shouldDragProp = h ? "horizontal" : "vertical";
		this.transform = h ? "translateX" : "translateY";
		this.dimension = h ? "width" : "height";
		this.boundary = h ? "left" : "top";
	},
	setInlineStyles: function(inValue, inDimensions) {
		var inBounds = {};

		if (this.unitModifier) {
			inBounds[this.boundary] = this.percentToPixels(inValue, this.unitModifier);
			inBounds[this.dimension] = this.unitModifier;
			this.setBounds(inBounds);
		} else {
			if (inDimensions) {
				inBounds[this.dimension] = inDimensions;
			} else {
				inBounds[this.boundary] = inValue;
			}
			this.setBounds(inBounds, this.unit);
		}
	},
	valueChanged: function(inLast) {
		var v = this.value;
		if (this.isOob(v) && !this.isAnimating()) {
			this.value = this.overMoving ? this.dampValue(v) : this.clampValue(v);
		}
		// FIXME: android cannot handle nested compositing well so apply acceleration only if needed
		// desktop chrome doesn't like this code path so avoid...
		if (enyo.platform.android > 2) {
			if (this.value) {
				if (inLast === 0 || inLast === undefined) {
					enyo.dom.accelerate(this, this.accelerated);
				}
			} else {
				enyo.dom.accelerate(this, false);
			}
		}

		// If platform supports transforms
		if (this.canTransform) {
			enyo.dom.transformValue(this, this.transform, this.value + this.unit);
		// else update inline styles
		} else {
			this.setInlineStyles(this.value, false);
		}
		this.doChange();
	},
	getAnimator: function() {
		return this.$.animator;
	},
	isAtMin: function() {
		return this.value <= this.calcMin();
	},
	isAtMax: function() {
		return this.value >= this.calcMax();
	},
	calcMin: function() {
		return this.min;
	},
	calcMax: function() {
		return this.max;
	},
	clampValue: function(inValue) {
		var min = this.calcMin();
		var max = this.calcMax();
		return Math.max(min, Math.min(inValue, max));
	},
	dampValue: function(inValue) {
		return this.dampBound(this.dampBound(inValue, this.min, 1), this.max, -1);
	},
	dampBound: function(inValue, inBoundary, inSign) {
		var v = inValue;
		if (v * inSign < inBoundary * inSign) {
			v = inBoundary + (v - inBoundary) / 4;
		}
		return v;
	},
	percentToPixels: function(value, dimension) {
		return Math.floor((dimension / 100) * value);
	},
	pixelsToPercent: function(value) {
		var boundary = this.unitModifier ? this.getBounds()[this.dimension] : this.container.getBounds()[this.dimension];
		return (value / boundary) * 100;
	},
	// dragging
	shouldDrag: function(inEvent) {
		return this.draggable && inEvent[this.shouldDragProp];
	},
	isOob: function(inValue) {
		return inValue > this.calcMax() || inValue < this.calcMin();
	},
	dragstart: function(inSender, inEvent) {
		if (this.shouldDrag(inEvent)) {
			inEvent.preventDefault();
			this.$.animator.stop();
			inEvent.dragInfo = {};
			this.dragging = true;
			this.drag0 = this.value;
			this.dragd0 = 0;
			return this.preventDragPropagation;
		}
	},
	drag: function(inSender, inEvent) {
		if (this.dragging) {
			inEvent.preventDefault();
			var d = this.canTransform ? inEvent[this.dragMoveProp] * this.kDragScalar : this.pixelsToPercent(inEvent[this.dragMoveProp]);
			var v = this.drag0 + d;
			var dd = d - this.dragd0;
			this.dragd0 = d;
			if (dd) {
				inEvent.dragInfo.minimizing = dd < 0;
			}
			this.setValue(v);
			return this.preventDragPropagation;
		}
	},
	dragfinish: function(inSender, inEvent) {
		if (this.dragging) {
			this.dragging = false;
			this.completeDrag(inEvent);
			inEvent.preventTap();
			return this.preventDragPropagation;
		}
	},
	completeDrag: function(inEvent) {
		if (this.value !== this.calcMax() && this.value != this.calcMin()) {
			this.animateToMinMax(inEvent.dragInfo.minimizing);
		}
	},
	// animation
	isAnimating: function() {
		return this.$.animator.isAnimating();
	},
	play: function(inStart, inEnd) {
		this.$.animator.play({
			startValue: inStart,
			endValue: inEnd,
			node: this.hasNode()
		});
	},
	//* @public
	//* Animates to the given value.
	animateTo: function(inValue) {
		this.play(this.value, inValue);
	},
	//* Animates to the minimum value.
	animateToMin: function() {
		this.animateTo(this.calcMin());
	},
	//* Animates to the maximum value.
	animateToMax: function() {
		this.animateTo(this.calcMax());
	},
	//* @protected
	animateToMinMax: function(inMin) {
		if (inMin) {
			this.animateToMin();
		} else {
			this.animateToMax();
		}
	},
	animatorStep: function(inSender) {
		this.setValue(inSender.value);
		return true;
	},
	animatorComplete: function(inSender) {
		this.doAnimateFinish(inSender);
		return true;
	},
	//* @public
	//* Toggles between _min_ and _max_ values with animation.
	toggleMinMax: function() {
		this.animateToMinMax(!this.isAtMin());
	}
});
