jQuery(function($) {
  var locals = {
    container: null,
    viewport: null,
    nav: null,
    slider: null,
    loading: null,
    constraint: null,
    draggable: null,
    img: null
  };
  var url;
  var img_width, img_height;
  var scale_dims = [];
  var scale_level;
  var max_scale_level;

  $.fn.dragzoom = function(settings) {
    if (! this) return false;
    if (! settings || ! settings.url) return false;
    url = settings.url;
    locals.container = this;
    construct();
    return false;
  };

  function construct() {
    locals.container.html(
      '<div id="dragzoomLoading">Downloading copyrighted larger image.<br>It may take several seconds...</div>' +
      '<div id="dragzoomViewport">' +
        '<div id="dragzoomNav">' +
	  '<div id="dragzoomZoomIn" />' +
	  '<div id="dragzoomSlider" />' +
	  '<div id="dragzoomZoomOut">' +
	  '</div>' +
	'</div>' +
        '<div id="dragzoomConstraint">' +
	  '<div id="dragzoomDraggable" class="ui-widget-content">' +
	    '<img id="dragzoomImg" src="'+url+'" />' +
	  '</div>' +
	'</div>' +
      '</div>'
    );
    $('#dragzoomZoomIn').click(function() { zoom(scale_level + 1); });
    $('#dragzoomZoomOut').click(function() { zoom(scale_level - 1); });

    locals.viewport = $('#dragzoomViewport');
    locals.nav = $('#dragzoomNav');
    locals.slider = $('#dragzoomSlider');
    locals.loading = $('#dragzoomLoading');
    locals.constraint = $('#dragzoomConstraint');
    locals.draggable = $('#dragzoomDraggable');
    locals.img = $('#dragzoomImg');
    locals.img.load(function() {
      init_viewport();
      show_draggable();
    });
    locals.img.hover(
      function() { $(this).css({'cursor': 'move'}); },
      function() { $(this).css({'cursor': 'default'}); }
    );
  }

  function init_viewport() {
    locals.loading.hide();
    locals.viewport.show();

    var img_aspect = locals.img.width() / locals.img.height();
    var viewport_aspect = locals.viewport.width() / locals.viewport.height();
    if (locals.img.width() <= locals.viewport.width() &&
	locals.img.height() <= locals.viewport.height()) {
      locals.viewport.width(locals.img.width());
      locals.viewport.height(locals.img.height());
      locals.viewport.css({
        top:  (locals.container.height() - locals.viewport.height()) / 2,
        left: (locals.container.width() - locals.viewport.width()) / 2
      });
    }
    // XXX3 - I'm not sure how well width() and height() deal with borders.
    //        test with thick ones.
    // XXX4 - this may not hold up with resizes.  May end up needing to do a
    //        locals.viewport.width(locals.viewport.width()) and ditto height.
    else if (img_aspect >= viewport_aspect) {
      locals.viewport.height(locals.viewport.width() / img_aspect);
      locals.viewport.css({
        top: (locals.container.height() - locals.viewport.height()) / 2
      });
    }
    else {
      locals.viewport.width(locals.viewport.height() * img_aspect);
      locals.viewport.css({
        left: (locals.container.width() - locals.viewport.width()) / 2
      });
    }

    var full_scale, scale_levels;
    full_scale = locals.viewport.width() / locals.img.width();
    scale_levels = Math.ceil(Math.log(1 / full_scale) / Math.log(1.3));
    if (scale_levels == 0) scale_levels = 1;
    var scale_fact = Math.pow(1 / full_scale, 1 / scale_levels);
    var scale_width = locals.viewport.width();
    var scale_height = locals.viewport.height();
    for (var i = 0; i < scale_levels; i++) {
      scale_dims[i] = {width: scale_width, height: scale_height};
      scale_width *= scale_fact;
      scale_height *= scale_fact;
    }
    max_scale_level = scale_levels - 1;
    scale_level = 0;

    if (scale_levels > 1) {
      locals.slider.height(20 + scale_levels * 10);
      locals.slider.slider({
	orientation: 'vertical',
	min: 0,
        max: scale_levels - 1,
	stop: function(event, ui) {
	  zoom(ui.value);
	}
      });

      locals.draggable.bind("dblclick", function(event) {
	var viewport_x = event.pageX - locals.viewport.offset().left;
	var viewport_y = event.pageY - locals.viewport.offset().top;
        locals.draggable.css({
	  left: locals.draggable.position().left + locals.viewport.width() / 2 - viewport_x,
	  top: locals.draggable.position().top + locals.viewport.height() / 2 - viewport_y
	});
	zoom(scale_level + 1);
        return false;
      });
      locals.draggable.rightClick(function(event) {
	var viewport_x = event.pageX - locals.viewport.offset().left;
	var viewport_y = event.pageY - locals.viewport.offset().top;
        locals.draggable.css({
	  left: locals.draggable.position().left + locals.viewport.width() / 2 - viewport_x,
	  top: locals.draggable.position().top + locals.viewport.height() / 2 - viewport_y
	});
	zoom(scale_level - 1);
        return false;
      });
    }
    else locals.nav.hide();

    img_width = locals.img.width();
    img_height = locals.img.height();
  }

  function show_draggable() {
    var old_img_width = locals.img.width();
    var old_img_height = locals.img.height();
    var new_img_width = scale_dims[scale_level].width;
    var new_img_height = scale_dims[scale_level].height;
    locals.img.width(new_img_width);
    locals.img.height(new_img_height);

    locals.constraint.width(Math.floor(2 * new_img_width - locals.viewport.width()));
    locals.constraint.height(Math.floor(2 * new_img_height - locals.viewport.height()));
    locals.constraint.css({
      left: locals.viewport.width() - locals.img.width(),
      top:  locals.viewport.height() - locals.img.height()
    });

    // Zoom in and out with the same point on the image centered in the
    // viewport (constraining to edges as needed).  After much algebra, this
    // is the best I could come up with.  There may be a simpler form based
    // on the fact that draggable.left/top is based on scale_fact etc., but
    // I didn't find it if so.
    var scale_fact = new_img_width / old_img_width;
    var new_left = scale_fact * locals.draggable.position().left +
		   locals.viewport.width() / 2 * (scale_fact - 1);
    var new_top = scale_fact * locals.draggable.position().top +
		  locals.viewport.height() / 2 * (scale_fact - 1);
    new_left = Math.max(new_left, 0);
    new_left = Math.min(new_left, locals.constraint.width() - new_img_width);
    new_top = Math.max(new_top, 0);
    new_top = Math.min(new_top, locals.constraint.height() - new_img_height);

    locals.draggable.width(locals.img.width());
    locals.draggable.height(locals.img.height());
    locals.draggable.css({
      left: new_left,
      top: new_top
    });
    locals.draggable.draggable({ containment: locals.constraint });

  }

  function zoom(level) {
    if (level < 0) level = 0;
    if (level > max_scale_level) level = max_scale_level;
    scale_level = level;
    locals.slider.slider('value', level);
    show_draggable();
  }
});

