$(function(){
//  $('img').preview({  // old way of doing it -- all images no matter what
  $('.images img[big_image]').preview({  // only do items with ALT attribute in a <div class="image"></div> 
    previewURL: function(el){
     // return el.attr('src').replace('thumb_', 'preview_');
     return el.attr('big_image');
    }
  });
});

(function($){
  Function.prototype.bind = function(obj){
    var self = this;
    return function(){
      self.apply(obj, Array.prototype.slice.call(arguments));
    };
  };

  $.isUndefined = function(t){
    return typeof(t) == 'undefined';
  };

  // $.isPlainObject has way too many checks =\
  $.isObject = function(t){
    return typeof(t) == 'object';
  };

  // stupid IE returns empty string instead of 0px and has default border-width = medium
  // thats a dirty hack to make 0 from NaN
  var parseInt0 = function(x){
    var num = parseInt(x);
    return isNaN(num) ? 0 : num;
  };

  $.visibleArea = function(){
    var area = {
      'left'   : $(document).scrollLeft(),
      'top'    : $(document).scrollTop(),
      'width'  : $(window).width(),
      'height' : window.innerHeight ? window.innerHeight : $(window).height()
    };

    area.right = area.left + area.width;
    area.bottom = area.top + area.height;

    return area;
  };

  $.fn.additionalWidth = function(){
    return parseInt0(this.css('border-left-width'))  +
           parseInt0(this.css('border-right-width')) +
           parseInt0(this.css('margin-right'))       +
           parseInt0(this.css('margin-left'));
  };

  $.fn.additionalHeight = function(){
    return parseInt0(this.css('border-bottom-width')) +
           parseInt0(this.css('border-top-width'))    +
           parseInt0(this.css('margin-top'))          +
           parseInt0(this.css('margin-bottom'));
  };

  $.fn.full_width = function(){
    return this.width() + this.additionalWidth();
  };

  $.fn.full_height = function(){
    return this.height() + this.additionalHeight();
  };

  var PreviewFrame = function(element, options){
    this.options = $.extend({}, PreviewFrame.defaultOptions, options || {});
    this.element = $(element);

    this.set_event_handlers();
    this.element.data('previewFrame', this);

    this.hook('initialized', this);
  };

  PreviewFrame.defaultOptions = {
    spinnerClass : 'preview-spinner',
    frameClass   : 'preview-frame',
    idPrefix     : 'preview-frame-',
    marginX      : 10,
    marginY      : 20,
    zIndex       : 1024,
    delay        : 400
  };

  PreviewFrame.count = 0;

  $.extend(PreviewFrame.prototype, {
    set_event_handlers: function(){
      var self = this;

      this.element.mouseover(function(){
        if(self.timeout || self.visible)
          return;

        self.timeout = setTimeout(function(){
          self.show();
        }, self.options.delay);
      });

      this.element.mouseout(function(){
        if(self.timeout){
          clearTimeout(self.timeout);
          self.timeout = null;
        }

        self.hide();
      });
    },

    preview_url: function(){
      var t;

      if(t = this.hook('previewURL', this.element))
        return t;

      if($.isObject(this.options.previewURL))
        return this.options.previewURL[this.element.attr('id')] ||
               this.options.previewURL[this.element.attr('src')];

      return this.options.previewURL;
    },

    make_spinner: function(postfix){
      this.spinner = $('<div></div>').addClass(this.options.spinnerClass);

      if(!$.isUndefined(this.options.spinnerID))
        this.spinner.attr('id', this.hook('spinnerID') || this.options.spinnerID);
      else if(!$.isUndefined(this.options.spinnerIDPrefix))
        this.spinner.attr('id', this.options.spinnerIDPrefix + postfix);

      // if spinner will get mouseover/mouseout this means target
      // element will get mouseover and will mark frame as hidden
      // but because spinner is exactly over target element we can say
      // that mouse is still over target element so mark frame to
      // be visible/hidden according to spinner events
      var self = this;
      this.spinner.mouseover(function(){
        self.visible = true;
      }).mouseout(function(){
        self.visible = false;
      });

      $('body').append(this.spinner);

      var pos = this.element.offset();

      this.spinner.css({
        'position' : 'absolute',
        'left'     : (pos.left - this.element.additionalWidth() + this.spinner.additionalHeight()) + 'px',
        'top'      : (pos.top - this.element.additionalHeight() + this.spinner.additionalHeight()) + 'px',
        'width'    : this.element.full_width() + 'px',
        'height'   : this.element.full_height() + 'px',
        'z-index'  : this.options.zIndex
      });

      this.hook('spinnerConstructed', this.spinner);
    },

    image_loaded: function(){
      if(this.spinner){
        this.spinner.remove();
        delete this.spinner;
      }

      this.reposition();

      if(!this.visible)
        return this.hide();

      this.frame.fadeIn();
      this.hook('frameShown', this.frame);
    },

    reposition: function(){
      var t, left, top;

      if(t = this.hook('position', this.element, this.frame)){
        if(t instanceof Array){
          left = t[0];
          top = t[1];
        }else{
          left = t.left;
          top = t.top;
        }
      }else{
        var pos = this.element.offset(), w = this.frame.full_width(), h = this.frame.full_height();
        pos.left -= this.element.additionalWidth();
        pos.top -= this.element.additionalHeight();

        left = pos.left + this.element.full_width() + this.options.marginX;
        top = pos.top + (this.element.full_height() / 2) - (h / 3);

        t = $.visibleArea();

        if(top + h > t.bottom - this.options.marginY)
          top = t.bottom - h - this.options.marginY;

        if(top < t.top + this.options.marginY)
          top = t.top + this.options.marginY;

        if(left + w > t.right)
          left = pos.left - this.options.marginX - w;
      }

      this.frame.css({
        'position' : 'absolute',
        'left'     : left + 'px',
        'top'      : top + 'px',
        'z-index'  : this.options.zIndex
      });

      this.hook('positioned', this.frame);
    },

    show: function(){
      this.visible = true;

      if(this.frame){
        this.image_loaded();
        return;
      }

      var previewURL = this.preview_url();

      if(!previewURL)
        return;

      var postfix = (this.element.attr('id') || ++PreviewFrame.count);
      var id = this.hook('previewID') || this.options.previewID || 'preview-frame-';

      this.frame = $('<div></div>').attr({
        'id': id + postfix
      }).addClass(this.options.frameClass);

      this.img = $('<img/>').load(this.image_loaded.bind(this));

      if(!$.isUndefined(this.options.imgClass))
        this.img.addClass(this.options.imgClass)

      if(!$.isUndefined(this.options.imgID))
        this.img.attr('id', this.hook('imgID', this.img) || this.imgID);
      else if(!$.isUndefined(this.options.imgPrefix))
        this.img.attr('id', this.options.imgPrefix + postfix);

      if($.isUndefined(this.options.constructSpinner))
        this.make_spinner(postfix);
      else
        this.hook('constructSpinner', this.element);

      this.frame.append(this.img.attr('src', previewURL));
      $('body').append(this.frame.hide());

      this.hook('imageConstructed', this.img);
      this.hook('frameConstructed', this.frame);
    },

    hide: function(){
      this.visible = false;

      if(this.frame){
        this.frame.fadeOut();
        this.hook('frameHidden', this.frame);
      }
    },

    // this method calls user-provided callbacks if such
    hook: function(name){
      return $.isFunction(this.options[name]) ?
                this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)) :
                undefined;
    }
  });

  $.fn.preview = function(opts){
    $(this).each(function(){
      new PreviewFrame($(this), opts);
    });
  };
})(jQuery);

