/*! flip - v1.1.2 - 2016-10-20
* https://github.com/nnattawat/flip
* Copyright (c) 2016 Nattawat Nonsung; Licensed MIT */
(function( $ ) {
    /*
     * Private attributes and method
     */







    // Function from David Walsh: http://davidwalsh.name/css-animation-callback licensed with http://opensource.org/licenses/MIT
    var whichTransitionEvent = function() {
        var t, el = document.createElement("fakeelement"),
            transitions = {
                "transition"      : "transitionend",
                "OTransition"     : "oTransitionEnd",
                "MozTransition"   : "transitionend",
                "WebkitTransition": "webkitTransitionEnd"
            };

        for (t in transitions) {
            if (el.style[t] !== undefined) {
                return transitions[t];
            }
        }
    };

    /*
     * Model declaration
     */
    var Flip = function($el, options, callback) {
        // Define default setting
        this.setting = {
            axis: "y",
            reverse: false,
            trigger: "click",
            speed: 500,
            forceHeight: false,
            forceWidth: false,
            autoSize: true,
            front: '.front',
            back: '.back'
        };

        this.setting = $.extend(this.setting, options);

        if (typeof options.axis === 'string' && (options.axis.toLowerCase() === 'x' || options.axis.toLowerCase() === 'y')) {
            this.setting.axis = options.axis.toLowerCase();
        }

        if (typeof options.reverse === "boolean") {
            this.setting.reverse = options.reverse;
        }

        if (typeof options.trigger === 'string') {
            this.setting.trigger = options.trigger.toLowerCase();
        }

        var speed = parseInt(options.speed);
        if (!isNaN(speed)) {
            this.setting.speed = speed;
        }

        if (typeof options.forceHeight === "boolean") {
            this.setting.forceHeight = options.forceHeight;
        }

        if (typeof options.forceWidth === "boolean") {
            this.setting.forceWidth = options.forceWidth;
        }

        if (typeof options.autoSize === "boolean") {
            this.setting.autoSize = options.autoSize;
        }

        if (typeof options.front === 'string' || options.front instanceof $) {
            this.setting.front = options.front;
        }

        if (typeof options.back === 'string' || options.back instanceof $) {
            this.setting.back = options.back;
        }

        // Other attributes
        this.element = $el;
        this.frontElement = this.getFrontElement();
        this.backElement = this.getBackElement();
        this.isFlipped = false;

        this.init(callback);
    };

    /*
     * Public methods
     */
    $.extend(Flip.prototype, {

        flipDone: function(callback) {
            var self = this;
            // Providing a nicely wrapped up callback because transform is essentially async
            self.element.one(whichTransitionEvent(), function() {
                self.element.trigger('flip:done');
                if (typeof callback === 'function') {
                    callback.call(self.element);
                }
            });
        },

        flip: function(callback) {
            if (this.isFlipped) {
                return;
            }

            this.isFlipped = true;

            var rotateAxis = "rotate" + this.setting.axis;
            this.frontElement.css({
                transform: rotateAxis + (this.setting.reverse ? "(-180deg)" : "(180deg)"),
                "z-index": "0"
            });

            this.backElement.css({
                transform: rotateAxis + "(0deg)",
                "z-index": "1"
            });
            this.flipDone(callback);
        },

        unflip: function(callback) {
            if (!this.isFlipped) {
                return;
            }

            this.isFlipped = false;

            var rotateAxis = "rotate" + this.setting.axis;
            this.frontElement.css({
                transform: rotateAxis + "(0deg)",
                "z-index": "1"
            });

            this.backElement.css({
                transform: rotateAxis + (this.setting.reverse ? "(180deg)" : "(-180deg)"),
                "z-index": "0"
            });
            this.flipDone(callback);
        },

        getFrontElement: function() {
            if (this.setting.front instanceof $) {
                return this.setting.front;
            } else {
                return this.element.find(this.setting.front);
            }
        },

        getBackElement: function() {
            if (this.setting.back instanceof $) {
                return this.setting.back;
            } else {
                return this.element.find(this.setting.back);
            }
        },

        init: function(callback) {
            var self = this;

            var faces = self.frontElement.add(self.backElement);
            var rotateAxis = "rotate" + self.setting.axis;
            var perspective = self.element["outer" + (rotateAxis === "rotatex" ? "Height" : "Width")]() * 2;
            var elementCss = {
                'perspective': perspective,
                'position': 'relative'
            };
            var backElementCss = {
                "transform": rotateAxis + "(" + (self.setting.reverse ? "180deg" : "-180deg") + ")",
                "z-index": "0",
                "position": "relative"
            };
            var faceElementCss = {
                "backface-visibility": "hidden",
                "transform-style": "preserve-3d",
                "position": "absolute",
                "z-index": "1"
            };

            if (self.setting.forceHeight) {
                faces.outerHeight(self.element.height());
            } else if (self.setting.autoSize) {
                faceElementCss.height = '100%';
            }

            if (self.setting.forceWidth) {
                faces.outerWidth(self.element.width());
            } else if (self.setting.autoSize) {
                faceElementCss.width = '100%';
            }

            // Back face always visible on Chrome #39
            if ((window.chrome || (window.Intl && Intl.v8BreakIterator)) && 'CSS' in window) {
                //Blink Engine, add preserve-3d to self.element
                elementCss["-webkit-transform-style"] = "preserve-3d";
            }


            faces.css(faceElementCss).find('*').css({
                "backface-visibility": "hidden"
            });

            self.element.css(elementCss);
            self.backElement.css(backElementCss);

            // #39
            // not forcing width/height may cause an initial flip to show up on
            // page load when we apply the style to reverse the backface...
            // To prevent self we first apply the basic styles and then give the
            // browser a moment to apply them. Only afterwards do we add the transition.
            setTimeout(function() {
                // By now the browser should have applied the styles, so the transition
                // will only affect subsequent flips.
                var speedInSec = self.setting.speed / 1000 || 0.5;
                faces.css({
                    "transition": "all " + speedInSec + "s ease-out"
                });

                // This allows flip to be called for setup with only a callback (default settings)
                if (typeof callback === 'function') {
                    callback.call(self.element);
                }

                // While this used to work with a setTimeout of zero, at some point that became
                // unstable and the initial flip returned. The reason for this is unknown but we
                // will temporarily use a short delay of 20 to mitigate this issue.
            }, 20);

            self.attachEvents();
        },

        clickHandler: function(event) {
            if (!event) { event = window.event; }
            if (this.element.find($(event.target).closest('button, a, input[type="submit"]')).length) {
                return;
            }

            if (this.isFlipped) {
                this.unflip();
            } else {
                this.flip();
            }
        },

        hoverHandler: function() {
            var self = this;
            self.element.off('mouseleave.flip');

            self.flip();

            setTimeout(function() {
                self.element.on('mouseleave.flip', $.proxy(self.unflip, self));
                if (!self.element.is(":hover")) {
                    self.unflip();
                }
            }, (self.setting.speed + 150));
        },

        attachEvents: function() {
            var self = this;
            if (self.setting.trigger === "click") {
                self.element.on($.fn.tap ? "tap.flip" : "click.flip", $.proxy(self.clickHandler, self));
            } else if (self.setting.trigger === "hover") {
                self.element.on('mouseenter.flip', $.proxy(self.hoverHandler, self));
                self.element.on('mouseleave.flip', $.proxy(self.unflip, self));
            }
        },

        flipChanged: function(callback) {
            this.element.trigger('flip:change');
            if (typeof callback === 'function') {
                callback.call(this.element);
            }
        },

        changeSettings: function(options, callback) {
            var self = this;
            var changeNeeded = false;

            if (options.axis !== undefined && self.setting.axis !== options.axis.toLowerCase()) {
                self.setting.axis = options.axis.toLowerCase();
                changeNeeded = true;
            }

            if (options.reverse !== undefined && self.setting.reverse !== options.reverse) {
                self.setting.reverse = options.reverse;
                changeNeeded = true;
            }

            if (changeNeeded) {
                var faces = self.frontElement.add(self.backElement);
                var savedTrans = faces.css(["transition-property", "transition-timing-function", "transition-duration", "transition-delay"]);

                faces.css({
                    transition: "none"
                });

                // This sets up the first flip in the new direction automatically
                var rotateAxis = "rotate" + self.setting.axis;

                if (self.isFlipped) {
                    self.frontElement.css({
                        transform: rotateAxis + (self.setting.reverse ? "(-180deg)" : "(180deg)"),
                        "z-index": "0"
                    });
                } else {
                    self.backElement.css({
                        transform: rotateAxis + (self.setting.reverse ? "(180deg)" : "(-180deg)"),
                        "z-index": "0"
                    });
                }
                // Providing a nicely wrapped up callback because transform is essentially async
                setTimeout(function() {
                    faces.css(savedTrans);
                    self.flipChanged(callback);
                }, 0);
            } else {
                // If we didnt have to set the axis we can just call back.
                self.flipChanged(callback);
            }
        }

    });

    /*
     * jQuery collection methods
     */
    $.fn.flip = function (options, callback) {
        if (typeof options === 'function') {
            callback = options;
        }

        if (typeof options === "string" || typeof options === "boolean") {
            this.each(function() {
                var flip = $(this).data('flip-model');

                if (options === "toggle") {
                    options = !flip.isFlipped;
                }

                if (options) {
                    flip.flip(callback);
                } else {
                    flip.unflip(callback);
                }
            });
        } else {
            this.each(function() {
                if ($(this).data('flip-model')) { // The element has been initiated, all we have to do is change applicable settings
                    var flip = $(this).data('flip-model');

                    if (options && (options.axis !== undefined || options.reverse !== undefined)) {
                        flip.changeSettings(options, callback);
                    }
                } else { // Init
                    $(this).data('flip-model', new Flip($(this), (options || {}), callback));
                }
            });
        }

        return this;
    };

}( jQuery ));