(function( $ ) {
  var flip = function($dom, callback) {
    $dom.data("flipped", true);

    var rotateAxis = "rotate" + $dom.data("axis");
    $dom.find($dom.data("front")).css({
      transform: rotateAxis + ($dom.data("reverse") ? "(-180deg)" : "(180deg)"),
      "z-index": "0"
    });

    $dom.find($dom.data("back")).css({
      transform: rotateAxis + "(0deg)",
      "z-index": "1"
    });

    //Providing a nicely wrapped up callback because transform is essentially async
     $dom.one(whichTransitionEvent(), function(){
        $(this).trigger('flip:done');
        if (callback !== undefined){
          callback.call(this);
        }
      });
  };

  var unflip = function($dom, callback) {
    $dom.data("flipped", false);

    var rotateAxis = "rotate" + $dom.data("axis");
    $dom.find($dom.data("front")).css({
      transform: rotateAxis + "(0deg)",
      "z-index": "1"
    });

    $dom.find($dom.data("back")).css({
      transform: rotateAxis + ($dom.data("reverse") ? "(180deg)" : "(-180deg)"),
      "z-index": "0"
    });

    //Providing a nicely wrapped up callback because transform is essentially async
     $dom.one(whichTransitionEvent(), function(){
        $(this).trigger('flip:done');
        if (callback !== undefined){
          callback.call(this);
        }
      });
  };
  // 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];
      }
    }
  };
  $.fn.flip = function(options, callback) {
    if (typeof options == 'function'){
      //This allows flip to be called for setup with only a callback (default settings)
      callback = options;
    }
    this.each(function(){
      var $dom = $(this);

        if (options !== undefined && (typeof(options) == "boolean" || typeof(options) == "string")) { // Force flip the DOM
          if (options == "toggle"){
            options = !$dom.data("flipped");
          }
          if (options) {
            flip($dom,callback);
          } else {
            unflip($dom,callback);
          }
          // //Providing a nicely wrapped up callback because transform is essentially async
          //  $(this).one(whichTransitionEvent(), function(){
          //     $(this).trigger('flip:done');
          //     if (callback !== undefined){
          //       callback.call(this);
          //     }
          //   });
        } else if (!$dom.data("initiated")){ //Init flipable DOM
          $dom.data("initiated", true);

          var settings = $.extend({
            axis: "y",
            reverse: false,
            trigger: "click",
            speed: 500,
            forceHeight: false,
            forceWidth: false,
            autoSize: true,
            front: 'auto',
            back: 'auto'
          }, options );

          //By defualt we first check for the old front and back selectors for backward compatibility
          //if they arent there we fall back to auto selecting the first and second div
          if (settings.front == "auto"){
            settings.front = ($dom.find('.front').length > 0)? '.front' : 'div:first-child';
          }else if (settings.front == "autostrict"){
            settings.front = 'div:first-child';
          }
          if (settings.back == "auto"){
            //Note, we must use the old 'div:first-child + div' for IE compatibility
            settings.back = ($dom.find('.back').length > 0)? '.back' : 'div:first-child + div';
          }else if (settings.back == "autostrict"){
            settings.back = 'div:first-child + div';
          }
          // save reverse and axis css to DOM for performing flip
          $dom.data("reverse", settings.reverse);
          $dom.data("axis", settings.axis);
          $dom.data("front", settings.front);
          $dom.data("back", settings.back);

          var rotateAxis = "rotate" + (settings.axis.toLowerCase() == "x" ? "x" : "y"), 
              perspective = $dom["outer" + (rotateAxis == "rotatex" ? "Height" : "Width")]() * 2;

          $dom.find($dom.data("back")).css({
            transform: rotateAxis + "(" + (settings.reverse? "180deg" : "-180deg") + ")"
          });

          $dom.css({
            perspective: perspective,
            position: "relative"
          });

          var speedInSec = settings.speed / 1000 || 0.5;
          var faces = $dom.find(settings.front).add(settings.back, $dom);
          if (settings.forceHeight) {faces.outerHeight($dom.height());} else if (settings.autoSize) {faces.css({'height': '100%'});}
          if (settings.forceWidth) {faces.outerWidth($dom.width());} else if (settings.autoSize) {faces.css({'width': '100%'});}
          faces.css({
            "backface-visibility": "hidden",
            "transform-style": "preserve-3d",
            position: "absolute",
            "z-index": "1"
          });
          faces.find('*').css({
            "backface-visibility": "hidden"
          });
          $dom.find($dom.data("back")).css({
            transform: rotateAxis + "(" + (settings.reverse? "180deg" : "-180deg") + ")",
            "z-index": "0"
          });
		  
		  // Back face always visible on Chrome #39
          if ((window.chrome || (window.Intl && Intl.v8BreakIterator)) && 'CSS' in window){
            //Blink Engine, add preserve-3d to $dom
			$dom.css({"-webkit-transform-style": "preserve-3d"});
          }
		  // /#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 this 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.
            faces.css({
              transition: "all " + speedInSec + "s ease-out"
            });
            if (callback !== undefined){
              callback.call(this);
            }
          //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);

          if (settings.trigger.toLowerCase() == "click") {
            $dom.on($.fn.tap ? "tap" : "click", function(event) {
              if (!event) {event = window.event;}
              if ($dom.find($(event.target).closest('button, a, input[type="submit"]')).length) {
                return;
              }

              if ($dom.data("flipped")) {
                unflip($dom);
              } else {
                flip($dom);
              }
            });
          }
          else if (settings.trigger.toLowerCase() == "hover") {
            var performFlip = function() {
              $dom.unbind('mouseleave', performUnflip);

              flip($dom);

              setTimeout(function() {
                $dom.bind('mouseleave', performUnflip);
                if (!$dom.is(":hover")) {
                  unflip($dom);
                }
              }, (settings.speed + 150));
            };

            var performUnflip = function() {
              unflip($dom);
            };

            $dom.mouseenter(performFlip);
            $dom.mouseleave(performUnflip);
          }
        }else{
          //The element has been initiated, all we have to do is change applicable settings
          if (options && (options.axis !== undefined || options.reverse !== undefined)){
            changeSettings.call(this,options,function(){
              $dom.trigger('flip:change');
              if (callback !== undefined){
                callback.call(this);
              }
            });
          }
      }
    });

    return this;
  };
  var changeSettings = function(options,callback){
    var changeNeeded = false;
    if (options.axis !== undefined && $(this).data("axis") != options.axis.toLowerCase()){
      $(this).data("axis", options.axis.toLowerCase());
      changeNeeded = true;
    }
    if (options.reverse !== undefined && $(this).data("reverse") != options.reverse){
      $(this).data("reverse", options.reverse);
      changeNeeded = true;
    }
    if (changeNeeded){
      var faces = $(this).find($(this).data("front")).add($(this).data("back"), $(this));
      var savedTrans = faces.css("transition");
      faces.css({
        transition: "none"
      });
      //Only setting the axis if it needs to be

      //options.axis = options.axis.toLowerCase();
      //$(this).data("axis", options.axis);

      //This sets up the first flip in the new direction automatically
      var rotateAxis = "rotate" + $(this).data("axis");
      if ($(this).data("flipped")){
        $(this).find($(this).data("front")).css({
          transform: rotateAxis + ($(this).data("reverse") ? "(-180deg)" : "(180deg)"),
          "z-index": "0"
        });
      }else{
        $(this).find($(this).data("back")).css({
          transform: rotateAxis + "(" + ($(this).data("reverse")? "180deg" : "-180deg") + ")",
          "z-index": "0"
        });
      }
      //Providing a nicely wrapped up callback because transform is essentially async
      setTimeout(function(){
        faces.css({
          transition: savedTrans
        });
          callback.call(this);
      }.bind(this),0);
    }else{
      //If we didnt have to set the axis we can just call back.
        setTimeout(callback.bind(this), 0);
    }
  };
}( jQuery ));