/* 
 * Copyright 2014-2017 Aidan Rogers 
 * https://github.com/godswearhats/jquery-ui-rotatable
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

/* globals define jQuery */
(function (factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module.
        define(['jquery'], factory)
    } else {
        // Browser globals
        factory(jQuery)
    }
}(function ($) {
    $.widget(
        'ui.rotatable', $.ui.mouse, {
            widgetEventPrefix: 'rotate',

            options: {
                angle: false,       // specify an angle in radians (for backward compatability)
                degrees: false,     // specify angle in degrees
                handle: false,      // an image to use for a handle
                handleOffset: {     // where the handle should appear
                    top: 0,
                    left: 0
                },
                radians: false,     // specify angle in radians
                rotate: null,       // a callback for during rotation
                rotationCenterOffset: {   // offset the center of the element for rotation
                    top: 0,
                    left: 0
                },
                snap: false,        // boolean flag, should the element snap to a certain rotation?
                start: null,        // callback when rotation starts
                step: 22.5,         // angle in degrees that the rotation should be snapped to
                stop: null,         // callback when rotation stops
                transforms: null,   // other transforms to performed on the element
                wheelRotate: true   // boolean flag, should the element rotate when the mousewheel is rotated?
            },

            // accessor for the angle in radians
            angle: function (angle) {
                if (angle === undefined) {
                    return this.options.angle
                }
                this.options.angle = angle
                this.elementCurrentAngle = angle
                this._performRotation(this.options.angle)
            },

            // calculates the element center if needed and returns it
            getElementCenter: function () {
                this.elementCenter = this._calculateElementCenter()
                return this.elementCenter
            },

            // accessor for the handle
            handle: function (handle) {
                if (handle === undefined) {
                    return this.options.handle
                }
                this.options.handle = handle
            },

            plugins: {},

            /* accessor for the center of rotation
            * takes an object with keys of top and left
            */
            rotationCenterOffset: function (offset) {
                if (offset === undefined) {
                    return this.options.rotationCenterOffset
                }
                if (offset.top !== null) {
                    this.options.rotationCenterOffset.top = offset.top
                }
                if (offset.left !== null) {
                    this.options.rotationCenterOffset.left = offset.left
                }
            },

            // listener for rotating the element
            rotateElement: function (event) {
                if (!this.element || this.element.disabled || this.options.disabled) {
                    return false
                }

                if (!event.which) {
                    this.stopRotate(event)
                    return false
                }

                var rotateAngle = this._calculateRotateAngle(event)
                var previousRotateAngle = this.elementCurrentAngle
                this.elementCurrentAngle = rotateAngle

                // Plugins callbacks need to be called first.
                this._propagate('rotate', event)

                if (this._propagate('rotate', event) === false) {
                    this.elementCurrentAngle = previousRotateAngle
                    return false
                }
                var ui = this.ui()
                if (this._trigger('rotate', event, ui) === false) {
                    this.elementCurrentAngle = previousRotateAngle
                    return false
                } else if (ui.angle.current !== rotateAngle) {
                    rotateAngle = ui.angle.current
                    this.elementCurrentAngle = rotateAngle
                }

                this._performRotation(rotateAngle)

                if (previousRotateAngle !== rotateAngle) {
                    this.hasRotated = true
                }

                return false
            },

            // listener for starting rotation
            startRotate: function (event) {
                var center = this.getElementCenter()
                var startXFromCenter = event.pageX - center.x
                var startYFromCenter = event.pageY - center.y
                this.mouseStartAngle = Math.atan2(startYFromCenter, startXFromCenter)
                this.elementStartAngle = this.elementCurrentAngle
                this.hasRotated = false

                this._propagate('start', event)

                $(document).on('mousemove', this.listeners.rotateElement)
                $(document).on('mouseup', this.listeners.stopRotate)

                return false
            },

            // listener for stopping rotation
            stopRotate: function (event) {
                if (!this.element || this.element.disabled) {
                    return
                }

                $(document).off('mousemove', this.listeners.rotateElement)
                $(document).off('mouseup', this.listeners.stopRotate)

                this.elementStopAngle = this.elementCurrentAngle

                this._propagate('stop', event)

                setTimeout(
                    function () {
                        this.element = false }, 10
                )
                return false
            },

            // listener for mousewheel rotation
            wheelRotate: function (event) {
                var angle = this._angleInRadians(Math.round(event.originalEvent.deltaY / 10))
                if (this.options.snap || event.shiftKey) {
                    angle = this._calculateSnap(angle)
                }
                angle = this.elementCurrentAngle + angle
                this.angle(angle)
                this._trigger('rotate', event, this.ui())
            },

            // for callbacks
            ui: function () {
                return {
                    api: this,
                    element: this.element,
                    angle: {
                        start: this.elementStartAngle,
                        current: this.elementCurrentAngle,
                        degrees: Math.abs(this._angleInDegrees(this.elementCurrentAngle)),
                        stop: this.elementStopAngle
                    }
                }
            },

            /* *********************** private functions ************************** */
            // calculates the radians for a given angle in degrees
            _angleInRadians: function (degrees) {
                return degrees * Math.PI / 180
            },

            // calculates the degrees for a given angle in radians
            _angleInDegrees: function (radians) {
                return radians * 180 / Math.PI
            },

            // calculates the center of the element
            _calculateElementCenter: function () {
                var elementOffset = this._getElementOffset()

                // Rotation center given via options
                if (this._isRotationCenterSet()) {
                    return {
                        x: elementOffset.left + this.rotationCenterOffset().left,
                        y: elementOffset.top + this.rotationCenterOffset().top
                    }
                }

                // Deduce rotation center from transform-origin
                if (this.element.css('transform-origin') !== undefined) {
                    var originPx = this.element.css('transform-origin').match(/([\d.]+)px +([\d.]+)px/)
                    if (originPx != null) {
                        return {
                            x: elementOffset.left + parseFloat(originPx[1]),
                            y: elementOffset.top + parseFloat(originPx[2])
                        }
                    }
                }

                // Default rotation center: middle of the element
                return {
                    x: elementOffset.left + this.element.width() / 2,
                    y: elementOffset.top + this.element.height() / 2
                }
            },

            // calculates the angle that the element should snap to and returns it in radians
            _calculateSnap: function (radians) {
                var degrees = this._angleInDegrees(radians)
                degrees = Math.round(degrees / this.options.step) * this.options.step
                return this._angleInRadians(degrees)
            },

            // calculates the angle to rotate the element to, based on input
            _calculateRotateAngle: function (event) {
                var center = this.getElementCenter()

                var xFromCenter = event.pageX - center.x
                var yFromCenter = event.pageY - center.y
                var mouseAngle = Math.atan2(yFromCenter, xFromCenter)
                var rotateAngle = mouseAngle - this.mouseStartAngle + this.elementStartAngle

                if (this.options.snap || event.shiftKey) {
                    rotateAngle = this._calculateSnap(rotateAngle)
                }

                return rotateAngle
            },

            // constructor
            _create: function () {
                var handle
                if (!this.options.handle) {
                    handle = $(document.createElement('div'))
                    handle.addClass('ui-rotatable-handle')
                    if (this.options.handleOffset.top !== 0 || this.options.handleOffset.left !== 0) {
                          handle.css('position', 'relative')
                          handle.css('top', this.options.handleOffset.top + 'px')
                          handle.css('left', this.options.handleOffset.left + 'px')
                    }
                } else {
                    handle = this.options.handle
                }

                this.listeners = {
                    rotateElement: $.proxy(this.rotateElement, this),
                    startRotate: $.proxy(this.startRotate, this),
                    stopRotate: $.proxy(this.stopRotate, this),
                    wheelRotate: $.proxy(this.wheelRotate, this)
                }

                if (this.options.wheelRotate) {
                    this.element.on('wheel', this.listeners.wheelRotate)
                }

                handle.draggable({ helper: 'clone', start: this._dragStart, handle: handle })
                handle.on('mousedown', this.listeners.startRotate)

                if (!handle.closest(this.element).length) {
                    handle.appendTo(this.element)
                }
                this.rotationCenterOffset(this.options.rotationCenterOffset)

                if (this.options.degrees) {
                    this.elementCurrentAngle = this._angleInRadians(this.options.degrees)
                }
                else {
                    this.elementCurrentAngle = this.options.radians || this.options.angle || 0
                }
                this._performRotation(this.elementCurrentAngle)
            },

            // destructor
            _destroy: function () {
                this.element.removeClass('ui-rotatable')
                this.element.find('.ui-rotatable-handle').remove()

                if (this.options.wheelRotate) {
                    this.element.off('wheel', this.listeners.wheelRotate)
                }
            },

            // used for the handle
            _dragStart: function (event) {
                if (this.element) {
                    return false
                }
            },

            // retrieves the element offset
            _getElementOffset: function () {
                this._performRotation(0)
                var offset = this.element.offset()
                this._performRotation(this.elementCurrentAngle)
                return offset
            },

            _getTransforms: function (angle) {
                var transforms = 'rotate(' + angle + 'rad)'

                if (this.options.transforms) {
                    transforms += ' ' + (function (transforms) {
                          var t = []
                        for (var i in transforms) {
                            if (transforms.hasOwnProperty(i) && transforms[i]) {
                                t.push(i + '(' + transforms[i] + ')')
                            }
                        }
                        return t.join(' ')
                    }(this.options.transforms))
                }
                return transforms
            },

            // checks to see if the element has a rotationCenterOffset set
            _isRotationCenterSet: function () {
                return (this.options.rotationCenterOffset.top !== 0 || this.options.rotationCenterOffset.left !== 0)
            },

            // performs the actual rotation on the element
            _performRotation: function (angle) {
                if (this._isRotationCenterSet()) {
                    this.element.css('transform-origin', this.options.rotationCenterOffset.left + 'px ' + this.options.rotationCenterOffset.top + 'px')
                    this.element.css('-ms-transform-origin', this.options.rotationCenterOffset.left + 'px ' + this.options.rotationCenterOffset.top + 'px') /* IE 9 */
                    this.element.css('-webkit-transform-origin', this.options.rotationCenterOffset.left + 'px ' + this.options.rotationCenterOffset + 'px') /* Chrome, Safari, Opera */
                }

                var transforms = this._getTransforms(angle)

                this.element.css('transform', transforms)
                this.element.css('-moz-transform', transforms)
                this.element.css('-webkit-transform', transforms)
                this.element.css('-o-transform', transforms)
            },

            // propagates events
            _propagate: function (n, event) {
                $.ui.plugin.call(this, n, [event, this.ui()]);
                (n !== 'rotate' && this._trigger(n, event, this.ui()))
            }
        }
    )

    return $.ui.rotatable
}))