var ImagePeeker = Class.create();
ImagePeeker.prototype = {
  initialize: function(options) {
    this.opts = Object.extend({
      className: 'imagepeeker',

      viewId: '_imagepeeker_view',
      spinnerSrc: '../image/imagepeeker/spinner.gif',
      fallbackSrc: '../image/imagepeeker/fallback.jpg',

      loadInterval: 100,
      loadTimeout: 3000,

      waitTimeout: 100,

      paddingWidth: 20,

      borderWidth: 4,
      borderColor: '#ccc',

      backgroundColor: '#fff'

    }, options || {});

    this.opts.frameWidth = this.opts.paddingWidth + this.opts.borderWidth;

    this.failure_to_load = 'failure_to_load';

    this.view = new ImagePeeker.View(this.opts);
    this.cache = $A([]);
    this.groups = $A([]);

    this.preloadImages();
  },

  preloadImages: function() {
    var onSuccess = function(group, anchor, image, opts) {
      this.cache[image.src] = image;
      if (group) {
        var viewHeight = image.height + (this.opts.frameWidth * 2);
        var top = this.absTop(anchor) - viewHeight;
        var bottom = this.absTop(anchor) + anchor.offsetHeight + viewHeight;
        this.groups[group].update(top, bottom);
      }
    };

    var boundOnFailure = function(image) {
      this.cache[image.src] = this.failure_to_load
    }.bind(this);

    var boundShow = this.show.bind(this);
    var boundHide = this.hide.bind(this);
    var dummyOnClick = function() {return false;};

    $$('.' + this.opts.className).each(function(anchor) {
      //anchor.setStyle({display: 'inline-block'}); // for get correct offsetHeight

      var group = this.groupOf(anchor);
      var anchorHeight = anchor.offsetHeight;

      if(group) this.groups[group] = this.groups[group] || new ImagePeeker.Group();

      new ImagePeeker.Loader(
        anchor.href,
        onSuccess.bind(this, group, anchor),
        boundOnFailure,
        this.opts
      );

      Event.observe(anchor, 'mouseover', boundShow, true);
      Event.observe(anchor, 'mouseout', boundHide, true);
      anchor.onclick = dummyOnClick;
    }.bind(this));
  },

  show: function(event) {
    var elm = Event.element(event);
    var anchor = Event.findElement(event, 'a');
    var group = this.groups[this.groupOf(anchor)];

    this.view.showSpinner(elm, group);

    this.doShow(elm, anchor, group);
  },

  doShow: function(elm, anchor, group) {
    var image = this.cache[anchor.href];

    if (image == this.failure_to_load) {
      this.view.showFallback(elm, group);
    }
    else if((typeof image) == 'object') {
      this.view.show(image, elm, group);
    }
    else {
      this.waitTimeoutId = setTimeout(this.doShow.bind(this, elm, anchor, group), this.opts.waitTimeout);
    }
  },

  hide: function(event) {
    if (this.waitTimeoutId) clearTimeout(this.waitTimeoutId);
    this.view.hide();
  },

  params: function(anchor) {
    var params = anchor.getAttribute('params');
    if(! params) return null;

    var ret = $H({});
    var iterator = function(param) {
      var p = param.split('=');
      ret[p.first()] = p.last();
    }.bind(this);

    params.split(',').each(iterator);
    return ret;
  },

  groupOf: function(anchor) {
    var params = this.params(anchor);
    return (params && params.imagepeeker_group) ? params.imagepeeker_group : null ;
  }
};

ImagePeeker.View = Class.create();
ImagePeeker.View.prototype = {
  initialize: function(options) {
    this.opts = options;

    this.body = (document.body || document.documentElement);

    this.view = null;
    this.spinner = null;
    this.fallback = null;

    this.initView();
    this.initImage();
  },

  show: function(image, elm, group) {
    if (this.notReady()) return null;

    this.view.src = image.src;
    this.view.width = image.width;
    this.view.height = image.height;

    this.view.setStyle({
      top: this.adjustTop(elm, image, group) + 'px',
      left: this.adjustLeft(elm, image) + 'px',
      width: image.width + 'px',
      height: image.height + 'px'
    });

    this.view.show();
  },

  showSpinner: function dummy() {},
  showFallback: function dummy() {},

  hide: function() {
    this.view.hide();
  },

  adjustTop: function(elm, image, group) {
    var viewHeight = image.height + (this.opts.frameWidth * 2);
    var northTop = this.absTop(elm) - viewHeight;
    var southTop = this.absTop(elm) + elm.offsetHeight;// + this.opts.frameWidth;

    var top = group ? group.top : northTop ;
    var bottom = group ? group.bottom : southTop + viewHeight;

    if (top < this.scrollTop()) {
      return southTop;
    }
    else {
      return northTop;
    }
  },

  adjustLeft: function(elm, image) {
    var viewWidth = image.width + (this.opts.frameWidth * 2);
    var fixedScrollWidth = this.scrollLeft() + this.body.clientWidth;

    var westLeft = this.scrollLeft() + this.opts.frameWidth;
    var eastLeft = fixedScrollWidth - viewWidth;
    var relativeLeft = Math.floor(this.absLeft(elm) + (elm.offsetWidth / 2)) - (viewWidth / 2);

    if (relativeLeft < this.scrollLeft()) {
      return westLeft;
    }
    else if (fixedScrollWidth < (relativeLeft + viewWidth)) {
      return eastLeft;
    }
    else {
      return relativeLeft;
    }
  },

  notReady: function() {
    return ! (this.spinner && this.fallback);
  },

  initView: function() {
    var img = document.createElement('img');
    img.id = this.opts.viewId;

    this.body.appendChild(img);

    this.view = $(img);
    this.view.setStyle({
      position: 'absolute',
      top: '-9999px',
      left: '-9999px',
      backgroundColor: this.opts.backgroundColor,
      border: this.opts.borderWidth + 'px solid ' + this.opts.borderColor,
      padding: this.opts.paddingWidth + 'px'
    });
    this.hide();
  },

  initImage: function() {
    var onSuccess = function onSuccess(type, image) {
      this[type] = image;
      this['show' + type.capitalize()] = this.show.curry(image);
    };

    var onFailure = function onFailure(type) {
      alert(type + ' file is not found');
    };

    var boundImageLoader = function(type) {
      new ImagePeeker.Loader(
        this.opts[type + 'Src'],
        onSuccess.bind(this, type),
        onFailure.bind(this, type),
        this.opts
      );
    }.bind(this);

    $A(['spinner', 'fallback']).each(boundImageLoader);
  }
};

ImagePeeker.Group = Class.create();
ImagePeeker.Group.prototype = {
  initialize: function() {
    this.top = Infinity;
    this.bottom = 0;
  },

  update: function(top, bottom){
    this.top = Math.min(this.top, top);
    this.bottom = Math.max(this.bottom, bottom);
  }
};

ImagePeeker.Util = {
  absTop: function(elm) {
    return elm.offsetParent ? elm.offsetTop + this.absTop(elm.offsetParent) : elm.offsetTop;
  },

  absLeft: function(elm) {
    return elm.offsetParent ? elm.offsetLeft + this.absLeft(elm.offsetParent) : elm.offsetLeft;
  },

  scrollTop: function() {
    return document.body.scrollTop || document.documentElement.scrollTop;
  },

  scrollLeft: function() {
    return document.body.scrollLeft || document.documentElement.scrollLeft;
  },

  scrollHeight: function() {
    return document.body.scrollHeight || document.documentElement.scrollHeight;
  },

  scrollWidth: function() {
    return document.body.scrollWidth || document.documentElement.scrollWidth;
  }
};

// mix-in
Object.extend(ImagePeeker.prototype, ImagePeeker.Util);
Object.extend(ImagePeeker.View.prototype, ImagePeeker.Util);

ImagePeeker.Loader = Class.create();
ImagePeeker.Loader.prototype = {
  initialize: function(src, onSuccess, onFailure, options) {
    this.opts = options;

    var image = new Image();
    image.src = src;

    this.intervalId = setInterval(function() {
      if (image.complete) {
        this.clearTimer();
        (image.width != 0) ? onSuccess(image, this.opts) : onFailure(image);
      }
    }.bind(this), this.opts.loadInterval);

    this.timeoutId = setTimeout(function() {
      this.clearTimer();
      onFailure(image);
    }.bind(this), this.opts.loadTimeout)
  },

  clearTimer: function() {
    if (this.intervalId) clearInterval(this.intervalId);
    if (this.timeoutId) clearTimeout(this.timeoutId);
  }
};

Event.observe(window, 'load', function() {
  // this may work on IE 5.x but reject that just in case
  var agent = navigator.userAgent.toLowerCase();
  var major = parseInt(navigator.appVersion);
  var is_ie = (agent.indexOf("msie") != -1) && (agent.indexOf("opera") == -1);
  var ie3 = major < 4;
  var ie4 = (major == 4) && (agent.indexOf("msie 4") != -1);
  var ie5 = (major == 4) && (agent.indexOf("msie 5.") != -1);
  var less_than_ie6 = is_ie && (ie3 || ie4 || ie5);

  if(! less_than_ie6) {
    new ImagePeeker();
  }
});

