/**
 * @author Fabien BIZOT
 * http://lafabrick.com/blog
 * http://twitter.com/fabienbizot
 */
package com.lafabrick.components
{
    import flash.events.Event;
    import flash.events.FocusEvent;
    import flash.events.KeyboardEvent;
    import flash.events.MouseEvent;
    import flash.geom.Point;
    import flash.ui.Keyboard;
    
    import mx.events.FlexEvent;
    import mx.events.SandboxMouseEvent;
    import mx.managers.IFocusManagerComponent;
    
    import spark.components.supportClasses.Range;
    import spark.effects.animation.Animation;
    import spark.effects.animation.MotionPath;
    import spark.effects.animation.SimpleMotionPath;
    import spark.effects.easing.Sine;
    import spark.events.TrackBaseEvent;
    
    [Event("change", type="flash.events.Event")]
    [Event("changeEnd", type="mx.events.FlexEvent")]
    [Event("changeStart", type="mx.events.FlexEvent")]
    [Event("thumbDrag", type="spark.events.TrackBaseEvent")]
    [Event("thumbPress", type="spark.events.TrackBaseEvent")]
    [Event("thumbRelease", type="spark.events.TrackBaseEvent")]
    
    [DefaultTriggerEvent("change")]
    
    [Style("slideDuration", type="Number", format="Time", inherit="no")]
    
    [Exclude("color", kind="style")]
    [Exclude("fontSize", kind="style")]
    [Exclude("fontWeight", kind="style")]
    [Exclude("textAlign", kind="style")]
    
    [AccessibilityClass("spark.accessibility.SliderBaseAccImpl")]
    
    [SkinState("normal")]
    [SkinState("disabled")]
    
    public class CircularSlider extends Range implements IFocusManagerComponent
    {
        public function CircularSlider()
        {
            super();
            addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
        }
        
        public static const CLOCKWISE:String = "clockwise";
        public static const COUNTERCLOCKWISE:String = "counterclockwise";
        
        [SkinPart("false")]
        public var thumb : PieButton;
        
        [SkinPart("false")]
        public var track : PieButton;
        
        private var _angle : Number;
        private var _startAngle : Number;
        
        private var _clickOffset:Point;  
        
        private var _animator:Animation = null;
        private var _slideToValue:Number;
        
        private var _rotationDirection : String;
        
        private var _isKeyDown:Boolean = false;
        
        public function get rotationDirection():String
        {
            return _rotationDirection;
        }
        [Inspectable("clockwise,counterclockwise",defaultValue="counterclockwise",type="String")]
        public function set rotationDirection(value:String):void
        {
            _rotationDirection = value;
            invalidateDisplayList();
        }

        public function get angle():Number
        {
            return _angle;
        }
        
        public function set angle(value:Number):void
        {
            _angle = value;
            
            invalidateDisplayList();
        }
        
        public function get startAngle():Number
        {
            return _startAngle;
        }
        
        public function set startAngle(value:Number):void
        {
            _startAngle = value;
            
            invalidateDisplayList();
        }
        
        override public function set maximum(value:Number):void
        {
            if (value == super.maximum)
                return;
            
            super.maximum = value;
            invalidateSkinState();
            invalidateDisplayList();
        }
        
        override public function set minimum(value:Number):void
        {
            if (value == super.minimum)
                return;
            
            super.minimum = value;
            invalidateSkinState();
            invalidateDisplayList();
        }
        
        [Bindable("valueCommit")]
        override public function get value():Number
        {
            return super.value;
        }
        override public function set value(newValue:Number):void
        {
            if (newValue == super.value)
                return;
            
            super.value = newValue;
            invalidateDisplayList();
        }
        
        override protected function setValue(value:Number):void
        {
            super.setValue(value);
            invalidateDisplayList();
        }
        
        override public function changeValueByStep(increase:Boolean = true):void
        {
            var prevValue:Number = this.value;
            
            super.changeValueByStep(increase);
            
            if (value != prevValue)
                dispatchEvent(new Event(Event.CHANGE));
        }
        
        override protected function getCurrentSkinState():String
        {
            return enabled ? "normal" : "disabled";
        }
        
        private function addedToStageHandler(event:Event):void
        {
            updateSkinDisplayList();
        }
        
        override protected function updateDisplayList(w:Number, h:Number):void
        {
            super.updateDisplayList(w, h);
            updateSkinDisplayList();
        }
        
        protected function updateSkinDisplayList():void 
        {
            if( track ) {
                track.angle = angle;
                track.startAngle = startAngle;
            }
            if( thumb ) {
                thumb.startAngle = valueToAngle();
            }
        }
        
        protected function valueToAngle() : Number
        {
            var nvalue : Number = value * ( angle-thumb.angle ) / maximum;
            
            if( rotationDirection == CLOCKWISE ) {
                return angle-thumb.angle-nvalue+startAngle;
            }

            return nvalue+startAngle;
        }
        
        protected function pointToValue(x:Number, y:Number):Number
        {
            if (!thumb || !track)
                return 0;
            
            
            var centerX : Number = ( getLayoutBoundsWidth() ) /2;
            var centerY : Number = ( getLayoutBoundsHeight() ) /2;
            
            var xoffset : Number = 0;
            var yoffset : Number = 0;
            if( width > height ) {
                yoffset = height/width;
            }
            if( width < height ) {
                xoffset = width/height;
            }
            
            var clickWidth : Number = x - centerX;
            var clickHeight : Number = -y + centerY;
            
            clickWidth *= 2-yoffset;
            clickHeight *= 2-xoffset;
            
            var radStartAngle : Number = startAngle/180*Math.PI;
            
            
            var newRadAngle : Number = Math.atan2( clickHeight, clickWidth );
            newRadAngle -= radStartAngle;
            
            
            if( newRadAngle < 0 ) {
                newRadAngle += (2*Math.PI);
            }
            /*if( newRadAngle > (2*Math.PI) ) {
                newRadAngle -= Math.PI*2;
            }*/
            var newAngle : Number = ( (newRadAngle) * 180 / Math.PI);
            if( newAngle < 0 ) {
                newAngle = 360+newAngle;
            }
            
            var val : Number = (newAngle*maximum)/angle;
            if( rotationDirection == CLOCKWISE ) {
                val = maximum-val;
            }

            if( newAngle > angle || newAngle < 0  ) {
                if( newAngle > angle+(360-angle)/2 ) {
                    return rotationDirection == CLOCKWISE ? maximum : minimum;
                }
                else {
                    return rotationDirection == CLOCKWISE ? minimum : maximum;
                }
            }

            return val;
        }
        
        override protected function partAdded(partName:String, instance:Object):void
        {
            super.partAdded(partName, instance);
            
            if (instance == thumb)
            {
                thumb.focusEnabled = false;
                thumb.addEventListener(MouseEvent.MOUSE_DOWN, thumb_mouseDownHandler);
                thumb.addEventListener(FlexEvent.UPDATE_COMPLETE, thumb_updateCompleteHandler);
                thumb.stickyHighlighting = true;
            }
            else if (instance == track)
            {
                track.focusEnabled = false;
                track.addEventListener(MouseEvent.MOUSE_DOWN, track_mouseDownHandler);
            }
        }

        override protected function partRemoved(partName:String, instance:Object):void
        {
            super.partRemoved(partName, instance);
            
            if (instance == thumb) {
                thumb.removeEventListener(MouseEvent.MOUSE_DOWN, thumb_mouseDownHandler);        
                thumb.removeEventListener(FlexEvent.UPDATE_COMPLETE, thumb_updateCompleteHandler);            
            }
            else if (instance == track) {
                track.removeEventListener(MouseEvent.MOUSE_DOWN, track_mouseDownHandler);
            }
        }
        
        override protected function focusInHandler(event:FocusEvent):void
        {
            super.focusInHandler(event);
            systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_WHEEL, system_mouseWheelHandler, true);
        }
        
        override protected function focusOutHandler(event:FocusEvent):void
        {
            super.focusOutHandler(event);
            systemManager.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_WHEEL, system_mouseWheelHandler, true);
        }

        protected function system_mouseWheelHandler(event:MouseEvent):void
        {
            if (!event.isDefaultPrevented())
            {
                var newValue:Number = nearestValidValue(value + event.delta * stepSize, stepSize);
                setValue(newValue);
                dispatchEvent(new Event(Event.CHANGE));
                event.preventDefault();
            }
        }

        protected function thumb_mouseDownHandler(event:MouseEvent):void
        {        
            systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_MOVE, system_mouseMoveHandler, true);
            systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_UP, system_mouseUpHandler, true);
            systemManager.getSandboxRoot().addEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE, system_mouseUpHandler);
            
            _clickOffset = thumb.globalToLocal(new Point(event.stageX, event.stageY));
            
            dispatchEvent(new TrackBaseEvent(TrackBaseEvent.THUMB_PRESS));
            dispatchEvent(new FlexEvent(FlexEvent.CHANGE_START));
            
        }
        
        protected function system_mouseMoveHandler(event:MouseEvent):void
        {
            if (!track)
                return;
            
            var point:Point = track.globalToLocal(new Point(event.stageX, event.stageY));
            var newValue:Number = pointToValue(point.x, point.y);

            newValue = nearestValidValue(newValue, snapInterval);
            
            if (newValue != value)
            {
                setValue(newValue); 
                dispatchEvent(new TrackBaseEvent(TrackBaseEvent.THUMB_DRAG));
                dispatchEvent(new Event(Event.CHANGE));
            }
            
            event.updateAfterEvent();
        }
        
        protected function system_mouseUpHandler(event:Event):void
        {
            systemManager.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_MOVE, system_mouseMoveHandler, true);
            systemManager.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_UP, system_mouseUpHandler, true);
            systemManager.getSandboxRoot().removeEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE, system_mouseUpHandler);
            
            dispatchEvent(new TrackBaseEvent(TrackBaseEvent.THUMB_RELEASE));
            dispatchEvent(new FlexEvent(FlexEvent.CHANGE_END));
        }
        
        protected function track_mouseDownHandler(event:MouseEvent):void 
        { 
            if (!enabled)
                return;
            
            var point:Point = track.globalToLocal(new Point(event.stageX, event.stageY));
            var newValue:Number = pointToValue(point.x, point.y);
            
            newValue = nearestValidValue(newValue, snapInterval);
            if (newValue != value)
            {
                var slideDuration:Number = getStyle("slideDuration");
                if (slideDuration != 0) {
                    if (!_animator)
                    {
                        _animator = new Animation();
                        
                        var animTarget:AnimationTarget = new AnimationTarget(animationUpdateHandler);
                        animTarget.endFunction = animationEndHandler;
                        _animator.animationTarget = animTarget;                    
                        _animator.easer = new Sine(0);
                    }
                    
                    if (_animator.isPlaying)
                        stopAnimation();
                    
                    _slideToValue = newValue;
                    _animator.duration = slideDuration * (Math.abs(value - _slideToValue) / (maximum - minimum));
                    _animator.motionPaths = new <MotionPath>[new SimpleMotionPath("value", value, _slideToValue)];
                    
                    dispatchEvent(new FlexEvent(FlexEvent.CHANGE_START));
                    _animator.play();
                }
                else {
                    setValue(newValue);
                    dispatchEvent(new Event(Event.CHANGE));
                }
            }
            
            event.updateAfterEvent();
        }
        
        private function animationUpdateHandler(animation:Animation):void
        {
            value = animation.currentValue["value"];
        }
        
        private function animationEndHandler(animation:Animation):void
        {
            setValue(_slideToValue);
            
            dispatchEvent(new Event(Event.CHANGE));
            dispatchEvent(new FlexEvent(FlexEvent.CHANGE_END));
        }
        
        private function stopAnimation():void
        {
            _animator.stop();
            
            setValue(nearestValidValue(value, snapInterval));
            
            dispatchEvent(new Event(Event.CHANGE));
            dispatchEvent(new FlexEvent(FlexEvent.CHANGE_END));
        }
        
        private function thumb_updateCompleteHandler(event:Event):void
        {
            updateSkinDisplayList();
            thumb.removeEventListener(FlexEvent.UPDATE_COMPLETE, thumb_updateCompleteHandler);
        }
        
        override protected function keyUpHandler(event:KeyboardEvent) : void
        {
            switch (event.keyCode)
            {
                case Keyboard.DOWN:
                case Keyboard.LEFT:
                case Keyboard.UP:
                case Keyboard.RIGHT:
                {
                    if (_isKeyDown)
                    {
                        // Dispatch "change" event only after a repeat occurs.
                        dispatchEvent(new FlexEvent(FlexEvent.CHANGE_END));
                        _isKeyDown = false;
                    }
                    event.preventDefault();
                    break;
                }
            }
        }
        
        override protected function keyDownHandler(event:KeyboardEvent):void
        {
            super.keyDownHandler(event);
            
            if (event.isDefaultPrevented())
                return;
            
            if (_animator && _animator.isPlaying)
                stopAnimation();
            
            var prevValue:Number = this.value;
            var newValue:Number;
            switch (event.keyCode)
            {
                case Keyboard.DOWN:
                case Keyboard.LEFT:
                {
                    newValue = nearestValidValue(value - stepSize, snapInterval);
                    
                    if (prevValue != newValue)
                    {
                        if (!_isKeyDown)
                        {
                            dispatchEvent(new FlexEvent(FlexEvent.CHANGE_START));
                            _isKeyDown = true;
                        }
                        setValue(newValue);
                        dispatchEvent(new Event(Event.CHANGE));
                    }
                    event.preventDefault();
                    break;
                }
                    
                case Keyboard.UP:
                case Keyboard.RIGHT:
                {
                    newValue = nearestValidValue(value + stepSize, snapInterval);
                    
                    if (prevValue != newValue)
                    {
                        if (!_isKeyDown)
                        {
                            dispatchEvent(new FlexEvent(FlexEvent.CHANGE_START));
                            _isKeyDown = true;
                        }
                        setValue(newValue);
                        dispatchEvent(new Event(Event.CHANGE));
                    }
                    event.preventDefault();
                    break;
                }
                    
                case Keyboard.HOME:
                {
                    value = minimum;
                    if (value != prevValue)
                        dispatchEvent(new Event(Event.CHANGE));
                    event.preventDefault();
                    break;
                }
                    
                case Keyboard.END:
                {
                    value = maximum;
                    if (value != prevValue)
                        dispatchEvent(new Event(Event.CHANGE));
                    event.preventDefault();
                    break;
                }
            }
        }
        
    }
}

import spark.effects.animation.Animation;
import spark.effects.animation.IAnimationTarget;

internal class AnimationTarget implements IAnimationTarget
{
    public var updateFunction:Function;
    public var startFunction:Function;
    public var stopFunction:Function;
    public var endFunction:Function;
    public var repeatFunction:Function;
    
    public function AnimationTarget(updateFunction:Function = null)
    {
        this.updateFunction = updateFunction;
    }
    
    public function animationStart(animation:Animation):void
    {
        if (startFunction != null)
            startFunction(animation);
    }
    
    public function animationEnd(animation:Animation):void
    {
        if (endFunction != null)
            endFunction(animation);
    }
    
    public function animationStop(animation:Animation):void
    {
        if (stopFunction != null)
            stopFunction(animation);
    }
    
    public function animationRepeat(animation:Animation):void
    {
        if (repeatFunction != null)
            repeatFunction(animation);
    }
    
    public function animationUpdate(animation:Animation):void
    {
        if (updateFunction != null)
            updateFunction(animation);
    }
    
}