var create_drag_box = function(div)
{
  var create_handle = function(cursor, style)
  {
    var handle = $(document.createElement("div"));
    handle.style.position = "absolute";
    handle.className = "frame-box-handle " + cursor;
    handle.frame_drag_cursor = cursor;

    handle.style.pointerEvents = "all";
    div.appendChild(handle);
    for(s in style)
    {
      handle.style[s] = style[s];
    }
    return handle;
  }

  /* Create the corner handles after the edge handles, so they're on top. */
  create_handle("n-resize", {top: "-5px", width: "100%", height: "10px"});
  create_handle("s-resize", {bottom: "-5px", width: "100%", height: "10px"});
  create_handle("w-resize", {left: "-5px", height: "100%", width: "10px"});
  create_handle("e-resize", {right: "-5px", height: "100%", width: "10px"});
  create_handle("nw-resize", {top: "-5px", left: "-5px", height: "10px", width: "10px"});
  create_handle("ne-resize", {top: "-5px", right: "-5px", height: "10px", width: "10px"});
  create_handle("sw-resize", {bottom: "-5px", left: "-5px", height: "10px", width: "10px"});
  create_handle("se-resize", {bottom: "-5px", right: "-5px", height: "10px", width: "10px"});
}

var apply_drag = function(dragging_mode, x, y, image_dimensions, box)
{
  var move_modes = {
    "move": { left: +1, top: +1, bottom: +1, right: +1 },
    "n-resize": { top: +1 },
    "s-resize": { bottom: +1 },
    "w-resize": { left: +1 },
    "e-resize": { right: +1 },
    "nw-resize": { top: +1, left: +1 },
    "ne-resize": { top: +1, right: +1 },
    "sw-resize": { bottom: +1, left: +1 },
    "se-resize": { bottom: +1, right: +1 }
  }
  var mode = move_modes[dragging_mode];
  var result = {
    left: box.left,
    top: box.top,
    width: box.width,
    height: box.height
  };
  var right = result.left + result.width;
  var bottom = result.top + result.height;

  if(dragging_mode == "move")
  {
    /* In move mode, clamp the movement.  In other modes, clip the size below. */
    x = clamp(x, -result.left, image_dimensions.width-right);
    y = clamp(y, -result.top, image_dimensions.height-bottom);
  }

  /* Apply the drag. */
  if(mode.top != null)     result.top += y * mode.top;
  if(mode.left != null)    result.left += x * mode.left;
  if(mode.right != null)   right += x * mode.right;
  if(mode.bottom != null)  bottom += y * mode.bottom;

  if(dragging_mode != "move")
  {
    /* Only clamp the dimensions that were modified. */
    if(mode.left != null)   result.left = clamp(result.left, 0, right-1);
    if(mode.top != null)    result.top = clamp(result.top, 0, bottom-1);
    if(mode.bottom != null) bottom = clamp(bottom, result.top+1, image_dimensions.height);
    if(mode.right != null)  right = clamp(right, result.left+1, image_dimensions.width);
  }

  result.width = right - result.left;
  result.height = bottom - result.top;
  return result;
}

/*
 * Given a frame, its post and an image, return the frame's rectangle scaled to
 * the size of the image.
 */
var frame_dimensions_to_image = function(frame, image, post)
{
  var result = {
    top: frame.source_top,
    left: frame.source_left,
    width: frame.source_width,
    height: frame.source_height
  };
  result.left *= image.width / post.width;
  result.top *= image.height / post.height;
  result.width *= image.width / post.width;
  result.height *= image.height / post.height;

  result.top = Math.round(result.top); result.left = Math.round(result.left);
  result.width = Math.round(result.width); result.height = Math.round(result.height);

  return result;
}

/*
 * Convert dimensions scaled to an image back to the source resolution.
 */
var frame_dimensions_from_image = function(frame, image, post)
{
  var result = {
    source_top: frame.top,
    source_left: frame.left,
    source_width: frame.width,
    source_height: frame.height
  };

  /* Scale the coordinates back into the source resolution. */
  result.source_top /= image.height / post.height;
  result.source_left /= image.width / post.width;
  result.source_height /= image.height / post.height;
  result.source_width /= image.width / post.width;

  result.source_top = Math.round(result.source_top); result.source_left = Math.round(result.source_left);
  result.source_width = Math.round(result.source_width); result.source_height = Math.round(result.source_height);
  return result;
}

FrameEditor = function(container, image_container, popup_container, options)
{
  this.container = container;
  this.popup_container = popup_container;
  this.image_container = image_container;
  this.options = options;
  this.show_corner_drag = true;

  this.image_frames = [];

  /* Event handlers which are set only while the tag editor is open: */
  this.open_handlers = [];

  /* Set up the four parts of the corner dragger. */
  var popup_parts = [".frame-editor-nw", ".frame-editor-ne", ".frame-editor-sw", ".frame-editor-se"];
  this.corner_draggers = [];
  for(var i = 0; i < popup_parts.length; ++i)
  {
    var part = popup_parts[i];
    var div = this.popup_container.down(part);
    var corner_dragger = new CornerDragger(div, part, {
      onUpdate: function() {
        this.update_frame_in_list(this.editing_frame);
        this.update_image_frame(this.editing_frame);
      }.bind(this)
    });
    this.corner_draggers.push(corner_dragger);
  }

  /* Create the main frame.  This sits on top of the image, receives mouse events and
   * holds the individual frames. */
  var div = $(document.createElement("div"));
  div.style.position = "absolute";
  div.style.left = "0";
  div.style.top = "0";
  div.className = "frame-editor-main-frame";
  this.image_container.appendChild(div);
  this.main_frame = div;
  this.main_frame.hide();

  /* Frame editor buttons: */
  this.container.down(".frame-editor-add").on("click", function(e) { e.stop(); this.add_frame(); }.bindAsEventListener(this));

  /* Buttons in the frame table: */
  this.container.on("click", ".frame-label", function(e, element) {
    e.stop();
    var frame_idx = element.up(".frame-row").frame_idx;
    this.focus(frame_idx);
  }.bind(this));

  this.container.on("click", ".frame-delete", function(e, element) {
    e.stop();
    var frame_idx = element.up(".frame-row").frame_idx;
    this.delete_frame(frame_idx);
  }.bind(this));

  this.container.on("click", ".frame-up", function(e, element) {
    e.stop();
    var frame_idx = element.up(".frame-row").frame_idx;
    this.move_frame(frame_idx, frame_idx-1);
  }.bind(this));

  this.container.on("click", ".frame-down", function(e, element) {
    e.stop();
    var frame_idx = element.up(".frame-row").frame_idx;
    this.move_frame(frame_idx, frame_idx+1);
  }.bind(this));

  this.container.down("table").on("change", function(e) {
    this.form_data_changed();
  }.bind(this));
}

FrameEditor.prototype.move_frame = function(frame_idx, frame_idx_target)
{
  var post = Post.posts.get(this.post_id);

  frame_idx_target = Math.max(frame_idx_target, 0);
  frame_idx_target = Math.min(frame_idx_target, post.frames_pending.length-1);
  if(frame_idx == frame_idx_target)
    return;

  var frame = post.frames_pending[frame_idx];
  post.frames_pending.splice(frame_idx, 1);
  post.frames_pending.splice(frame_idx_target, 0, frame);

  this.repopulate_table();

  /* Reset the focus.  If the item that was moved was focused, focus on it in
   * its new position. */
  var editing_frame = this.editing_frame == frame_idx? frame_idx_target:this.editing_frame;
  this.editing_frame = null;
  this.focus(editing_frame);
}

FrameEditor.prototype.form_data_changed = function()
{
  var post = Post.posts.get(this.post_id);
  for(var i = 0; i < post.frames_pending.length; ++i)
    this.update_frame_from_list(i);
  this.update();
}

FrameEditor.prototype.set_drag_to_create = function(enable)
{
  this.drag_to_create = enable;
}

FrameEditor.prototype.update_show_corner_drag = function()
{
  var shown = this.post_id != null && this.editing_frame != null && this.show_corner_drag;
  if(Prototype.Browser.WebKit)
  {
    /* Work around a WebKit (maybe just a Chrome) issue.  Images are downloaded immediately, but
     * they're only decompressed the first time they're actually painted on screen.  This happens
     * late, after all style is applied: hiding with display: none, visibility: hidden or even
     * opacity: 0 causes the image to not be decoded until it's displayed, which causes a huge
     * UI hitch the first time the user drags a box.  Work around this by setting opacity very
     * small; it'll trick it into decoding the image, but it'll clip to 0 when rendered. */
    if(shown)
    {
      this.popup_container.style.opacity = 1;
      this.popup_container.style.pointerEvents = "";
      this.popup_container.style.position = "static";
    }
    else
    {
      this.popup_container.style.opacity = 0.001;

      /* Make sure the invisible element doesn't interfere with the page; disable pointer-events
       * so it doesn't receive clicks, and set it to absolute so it doesn't affect the size of its
       * containing box. */
      this.popup_container.style.pointerEvents = "none";
      this.popup_container.style.position = "absolute";
      this.popup_container.style.top = "0px";
      this.popup_container.style.right = "0px";
    }
    this.popup_container.show();
  }
  else
  {
    this.popup_container.show(shown);
  }

  for(var i = 0; i < this.corner_draggers.length; ++i)
    this.corner_draggers[i].update();
}

FrameEditor.prototype.set_show_corner_drag = function(enable)
{
  this.show_corner_drag = enable;
  this.update_show_corner_drag();
}

FrameEditor.prototype.set_image_dimensions = function(width, height)
{
  var editing_frame = this.editing_frame;
  var post_id = this.post_id;

  this.close();

  this.image_dimensions = {width: width, height: height};
  this.main_frame.style.width = this.image_dimensions.width + "px";
  this.main_frame.style.height = this.image_dimensions.height + "px";

  if(post_id != null)
  {
    this.open(post_id);
    this.focus(editing_frame);
  }
}

/*
 * Like document.elementFromPoint, but returns an array of all elements at the given point.
 * If a top element is specified, stop if it's reached without including it in the list.
 *
 */
var elementArrayFromPoint = function(x, y, top)
{
  var elements = [];
  while(1)
  {
    var element = document.elementFromPoint(x, y);
    if(element == this.main_frame || element == document.documentElement)
      break;
    element.original_display = element.style.display;
    element.style.display = "none";
    elements.push(element);
  }

  /* Restore the elements we just hid. */
  elements.each(function(e) {
    e.style.display = e.original_display;
    e.original_display = null;
  });

  return elements;
}

FrameEditor.prototype.is_opened = function()
{
  return this.post_id != null;
}

/* Open the frame editor if it isn't already, and focus on the specified frame. */
FrameEditor.prototype.open = function(post_id)
{
  if(this.image_dimensions == null)
    throw "Must call set_image_dimensions before open";
  if(this.post_id != null)
    return;
  this.post_id = post_id;
  this.editing_frame = null;
  this.dragging_item = null;

  this.container.show();
  this.main_frame.show();
  this.update_show_corner_drag();

  var post = Post.posts.get(this.post_id);

  /* Tell the corner draggers which post we're working on now, so they'll start
   * loading the JPEG version immediately if necessary.  Otherwise, we'll start
   * loading it the first time we focus a frame, which will hitch the editor for
   * a while in Chrome. */
  for(var i = 0; i < this.corner_draggers.length; ++i)
    this.corner_draggers[i].set_post_id(this.post_id);

  this.open_handlers.push(
    document.on("keydown", function(e) {
      if (e.keyCode == Event.KEY_ESC) { this.discard(); }
    }.bindAsEventListener(this))
  )

  /* If we havn't done so already, make a backup of this post's frames.  We'll restore
   * from this later if the user cancels the edit. */
  this.original_frames = Object.toJSON(post.frames_pending);

  this.repopulate_table();

  this.create_dragger();

  if(post.frames_pending.length > 0)
    this.focus(0);

  this.update();
}

FrameEditor.prototype.create_dragger = function()
{
  if(this.dragger)
    this.dragger.destroy();

  this.dragger = new DragElement(this.main_frame, {
    ondown: function(e) {
      var post = Post.posts.get(this.post_id);

      /*
       * Figure out which element(s) we're clicking on.  The click may lie on a spot
       * where multiple frames overlap; make a list.
       *
       * Temporarily enable pointerEvents on the frames, so elementFromPoint will
       * resolve them.
       */
      this.image_frames.each(function(frame) { frame.style.pointerEvents = "all"; });
      var clicked_elements = elementArrayFromPoint(e.x, e.y, this.main_frame);
      this.image_frames.each(function(frame) { frame.style.pointerEvents = "none"; });

      /* If we clicked on a handle, prefer it over frame bodies at the same spot. */
      var element = null;
      clicked_elements.each(function(e) {
        /* If a handle was clicked, always prefer it.  Use the first handle we find,
         * so we prefer the corner handles (which are always on top) to edge handles. */
        if(element == null && e.hasClassName("frame-box-handle"))
          element = e;
      }.bind(this));

      /* If a handle wasn't clicked, prefer the frame that's currently focused. */
      if(element == null)
      {
        clicked_elements.each(function(e) {
          if(!e.hasClassName("frame-editor-frame-box"))
            e = e.up(".frame-editor-frame-box");
          if(this.image_frames.indexOf(e) == this.editing_frame)
            element = e;
        }.bind(this));
      }

      /* Otherwise, just use the first item that was found. */
      if(element == null)
        element = clicked_elements[0];

      /* If a handle was clicked on, find the frame element that contains it. */
      var frame_element = element;
      if(!frame_element.hasClassName("frame-editor-frame-box"))
        frame_element = frame_element.up(".frame-editor-frame-box");

      /* If we didn't click on a frame box at all, create a new one. */
      if(frame_element == null)
      {
        if(!this.drag_to_create)
          return;

        this.dragging_new = true;
      }
      else
        this.dragging_new = false;

      /* If the element we actually clicked on was one of the edge handles, set the drag
       * mode based on which one was clicked. */
      if(element.hasClassName("frame-box-handle"))
        this.dragging_mode = element.frame_drag_cursor
      else
        this.dragging_mode = "move";

      if(frame_element && frame_element.hasClassName("frame-editor-frame-box"))
      {
        var frame_idx = this.image_frames.indexOf(frame_element);
        this.dragging_idx = frame_idx;

        var frame = post.frames_pending[this.dragging_idx];
        this.dragging_anchor = frame_dimensions_to_image(frame, this.image_dimensions, post);
      }

      this.focus(this.dragging_idx);

      /* If we're dragging a handle, override the drag class so the pointer will
       * use the handle pointer instead of the drag pointer. */
      this.dragger.overriden_drag_class = this.dragging_mode == "move"? null: this.dragging_mode;

      this.dragger.options.snap_pixels = this.dragging_new? 10:0;

      /* Stop propagation of the event, so any other draggers in the chain don't start.  In
       * particular, when we're dragging inside the image, we need to stop WindowDragElementAbsolute.
       * Only do this if we're actually dragging, not if we aborted due to this.drag_to_create. */
      e.latest_event.stopPropagation();
    }.bind(this),

    onup: function(e) {
      this.dragging_idx = null;
      this.dragging_anchor = null;
    }.bind(this),

    ondrag: function(e) {
      var post = Post.posts.get(this.post_id);

      if(this.dragging_new)
      {
        /* Pick a dragging mode based on which way we were dragged.  This is a
         * little funny; we should probably be able to drag freely, not be fixed
         * to the first direction we drag. */
        if(e.aX > 0 && e.aY > 0)        this.dragging_mode = "se-resize";
        else if(e.aX > 0 && e.aY < 0)   this.dragging_mode = "ne-resize";
        else if(e.aX < 0 && e.aY > 0)   this.dragging_mode = "sw-resize";
        else if(e.aX < 0 && e.aY < 0)   this.dragging_mode = "nw-resize";
        else return;

        this.dragging_new = false;

        /* Create a new, empty frame.  When we get to the regular drag path below we'll
         * give it its real size, based on how far we've dragged so far. */
        var frame_offset = this.main_frame.cumulativeOffset();
        var dims = {
          left: e.dragger.anchor_x - frame_offset.left,
          top: e.dragger.anchor_y - frame_offset.top,
          height: 0,
          width: 0
        };
        this.dragging_anchor = dims;

        var source_dims = frame_dimensions_from_image(dims, this.image_dimensions, post);
        this.dragging_idx = this.add_frame(source_dims);
        post.frames_pending[this.editing_frame] = source_dims;
      }

      if(this.dragging_idx == null)
        return;

      var dims = apply_drag(this.dragging_mode, e.aX, e.aY, this.image_dimensions, this.dragging_anchor);

      /* Scale the changed dimensions back to the source resolution and apply them
       * to the frame. */
      var source_dims = frame_dimensions_from_image(dims, this.image_dimensions, post);
      post.frames_pending[this.editing_frame] = source_dims;

      this.update_frame_in_list(this.editing_frame);
      this.update_image_frame(this.editing_frame);
    }.bind(this)
  });
}

FrameEditor.prototype.repopulate_table = function()
{
  var post = Post.posts.get(this.post_id);

  /* Clear the table. */
  var tbody = this.container.down(".frame-list").down("TBODY");
  while(tbody.firstChild)
    tbody.removeChild(tbody.firstChild);

  /* Clear the image frames. */
  this.image_frames.each(function(f) {
    f.parentNode.removeChild(f);
  }.bind(this));
  this.image_frames = [];

  for(var i = 0; i < post.frames_pending.length; ++i)
  {
    this.add_frame_to_list(i);
    this.create_image_frame();
    this.update_image_frame(i);
  }
}

FrameEditor.prototype.update = function()
{
  this.update_show_corner_drag();

  if(this.image_dimensions == null)
    return;

  var post = Post.posts.get(this.post_id);
  if(post != null)
  {
    for(var i = 0; i < post.frames_pending.length; ++i)
      this.update_image_frame(i);
  }
}

/* If the frame editor is open, discard changes and close it. */
FrameEditor.prototype.discard = function()
{
  if(this.post_id == null)
    return;

  /* Save revert_to, and close the editor before reverting, to make sure closing
   * the editor doesn't change anything. */
  var revert_to = this.original_frames;
  var post_id = this.post_id;
  this.close();

  /* Revert changes. */
  var post = Post.posts.get(post_id);
  post.frames_pending = revert_to.evalJSON();
}

/* Get the frames specifier for the post's frames. */
FrameEditor.prototype.get_current_frames_spec = function()
{
  var post = Post.posts.get(this.post_id);
  var frame = post.frames_pending;
  var frame_specs = [];
  post.frames_pending.each(function(frame) {
    var s = frame.source_left + "x" + frame.source_top + "," + frame.source_width + "x" + frame.source_height;
    frame_specs.push(s);
  }.bind(this));
  return frame_specs.join(";");
}


/* Return true if the frames have been changed. */
FrameEditor.prototype.changed = function()
{
  var post = Post.posts.get(this.post_id);
  var spec = this.get_current_frames_spec();
  return spec != post.frames_pending_string;
}

/* Save changes to the post, if any.  If not null, call finished on completion. */
FrameEditor.prototype.save = function(finished)
{
  if(this.post_id == null)
  {
    if(finished)
      finished();
    return;
  }

  /* Save the current post_id, so it's preserved when the AJAX completion function
   * below is run. */
  var post_id = this.post_id;
  var post = Post.posts.get(post_id);
  var frame = post.frames_pending;

  var spec = this.get_current_frames_spec();
  if(spec == post.frames_pending_string)
  {
    if(finished)
      finished();
    return;
  }

  Post.update_batch([{
    id: post_id,
    frames_pending_string: spec
  }], function(posts)
  {
    if(this.post_id == post_id)
    {
      /* The registered post has been changed, and we're still displaying it.  Grab the
       * new version, and updated original_frames so we no longer consider this post
       * changed. */
      var post = Post.posts.get(post_id);
      this.original_frames = Object.toJSON(post.frames_pending);

      /* In the off-chance that the frames_pending that came back differs from what we
       * requested, update the display. */
      this.update();
    }

    if(finished)
      finished();
  }.bind(this));
}

FrameEditor.prototype.create_image_frame = function()
{
  var div = $(document.createElement("div"));
  div.className = "frame-editor-frame-box";

  /* Disable pointer-events on the image frame, so the handle cursors always
   * show up even when an image frame lies on top of it. */
  div.style.pointerEvents = "none";

  // div.style.opacity=0.1;
  this.main_frame.appendChild(div);
  this.image_frames.push(div);

  create_drag_box(div);


}

FrameEditor.prototype.update_image_frame = function(frame_idx)
{
  var post = Post.posts.get(this.post_id);
  var frame = post.frames_pending[frame_idx];

  /* If the focused frame is being modified, update the corner dragger as well. */
  if(frame_idx == this.editing_frame)
  {
    for(var i = 0; i < this.corner_draggers.length; ++i)
      this.corner_draggers[i].update();
  }

  var dimensions = frame_dimensions_to_image(frame, this.image_dimensions, post);

  var div = this.image_frames[frame_idx];
  div.style.left = dimensions.left + "px";
  div.style.top = dimensions.top + "px";
  div.style.width = dimensions.width + "px";
  div.style.height = dimensions.height + "px";

  if(frame_idx == this.editing_frame)
    div.addClassName("focused-frame-box");
  else
    div.removeClassName("focused-frame-box");
}

/* Append the given frame to the editor list. */
FrameEditor.prototype.add_frame_to_list = function(frame_idx)
{
  var tbody = this.container.down(".frame-list").down("TBODY");
  var tr = $(document.createElement("TR"));
  tr.className = "frame-row frame-" + frame_idx;
  tr.frame_idx = frame_idx;
  tbody.appendChild(tr);

  var html = "<td><span class='frame-label'>Frame " + frame_idx + "</span></td>";
  html += "<td><input class='frame-left frame-dims' size=4></td>";
  html += "<td><input class='frame-top frame-dims' size=4></td>";
  html += "<td><input class='frame-width frame-dims' size=4></td>";
  html += "<td><input class='frame-height frame-dims' size=4></td>";
  html += "<td><a class='frame-delete frame-button-box' href='#'>X</a></td>";
  html += "<td><a class='frame-up frame-button-box' href='#'>⇡</a></td>";
  html += "<td><a class='frame-down frame-button-box' href='#'>⇣</a></td>";
  tr.innerHTML = html;

  this.update_frame_in_list(frame_idx);
}

/* Update the fields of frame_idx in the table. */
FrameEditor.prototype.update_frame_in_list = function(frame_idx)
{
  var post = Post.posts.get(this.post_id);
  var frame = post.frames_pending[frame_idx];

  var tbody = this.container.down(".frame-list").down("TBODY");
  var tr = tbody.down(".frame-" + frame_idx);

  tr.down(".frame-left").value = frame.source_left;
  tr.down(".frame-top").value = frame.source_top;
  tr.down(".frame-width").value = frame.source_width;
  tr.down(".frame-height").value = frame.source_height;
}

/* Commit changes in the frame list to the frame. */
FrameEditor.prototype.update_frame_from_list = function(frame_idx)
{
  var post = Post.posts.get(this.post_id);
  var frame = post.frames_pending[frame_idx];

  var tbody = this.container.down(".frame-list").down("TBODY");
  var tr = tbody.down(".frame-" + frame_idx);

  frame.source_left = tr.down(".frame-left").value;
  frame.source_top = tr.down(".frame-top").value;
  frame.source_width = tr.down(".frame-width").value;
  frame.source_height = tr.down(".frame-height").value;
}

/* Add a new default frame to the end of the list, update the table, and edit the new frame. */
FrameEditor.prototype.add_frame = function(new_frame)
{
  var post = Post.posts.get(this.post_id);

  if(new_frame == null)
    new_frame = {
      source_top: post.height * 1/4,
      source_left: post.width * 1/4,
      source_width: post.width / 2,
      source_height: post.height / 2
    };

  post.frames_pending.push(new_frame);
  this.add_frame_to_list(post.frames_pending.length-1);
  this.create_image_frame();
  this.update_image_frame(post.frames_pending.length-1);

  this.focus(post.frames_pending.length-1);
  return post.frames_pending.length-1;
}

/* Delete the specified frame. */
FrameEditor.prototype.delete_frame = function(frame_idx)
{
  var post = Post.posts.get(this.post_id);

  /* If we're editing this frame, switch to a nearby one. */
  var switch_to_frame = null;
  if(this.editing_frame == frame_idx)
  {
    switch_to_frame = this.editing_frame;
    this.focus(null);

    /* If we're deleting the bottom item on the list, switch to the item above it instead. */
    if(frame_idx == post.frames_pending.length-1)
      --switch_to_frame;

    /* If that put it over the top, we're deleting the only item.  Focus no item. */
    if(switch_to_frame < 0)
      switch_to_frame = null;
  }

  /* Remove the frame from the array. */
  post.frames_pending.splice(frame_idx, 1);

  /* Renumber the table. */
  this.repopulate_table();

  /* Focus switch_to_frame, if any. */
  this.focus(switch_to_frame);
}

FrameEditor.prototype.focus = function(post_frame)
{
  if(this.editing_frame == post_frame)
    return;

  if(this.editing_frame != null)
  {
    var row = this.container.down(".frame-" + this.editing_frame);
    row.removeClassName("frame-focused");
  }

  this.editing_frame = post_frame;

  if(this.editing_frame != null)
  {
    var row = this.container.down(".frame-" + this.editing_frame);
    row.addClassName("frame-focused");
  }

  for(var i = 0; i < this.corner_draggers.length; ++i)
    this.corner_draggers[i].set_post_frame(this.editing_frame);

  this.update();
}

/* Close the frame editor.  Local changes are not saved or reverted. */
FrameEditor.prototype.close = function()
{
  if(this.post_id == null)
    return;
  this.post_id = null;

  this.editing_frame = null;

  for(var i = 0; i < this.corner_draggers.length; ++i)
    this.corner_draggers[i].set_post_id(null);

  if(this.keydown_handler)
  {
    this.open_handlers.each(function(h) { h.stop(); });
    this.open_handlers = [];
  }

  if(this.dragger)
    this.dragger.destroy();
  this.dragger = null;

  this.container.hide();
  this.main_frame.hide();
  this.update_show_corner_drag();

  /* Clear the row table. */
  var tbody = this.container.down(".frame-list").down("TBODY");
  while(tbody.firstChild)
    tbody.removeChild(tbody.firstChild);

  this.original_frames = null;
  this.update();

  if(this.options.onClose)
    this.options.onClose(this);
}

/* Create the specified corner dragger. */
CornerDragger = function(container, part, options)
{
  this.container = container;
  this.part = part;
  this.options = options;

  var box = container.down(".frame-editor-popup-div");

  /* Create a div inside each .frame-editor-popup-div floating on top of the image
   * to show the border of the frame. */
  var frame_box = $(document.createElement("div"));
  frame_box.className = "frame-editor-frame-box";
  create_drag_box(frame_box);
  box.appendChild(frame_box);

  this.dragger = new DragElement(box, {
    snap_pixels: 0,

    ondown: function(e) {
      var element = document.elementFromPoint(e.x, e.y);

      /* If we clicked on a drag handle, use that handle.  Otherwise, choose the corner drag
       * handle for the corner we're in. */
      if(element.hasClassName("frame-box-handle")) this.dragging_mode = element.frame_drag_cursor;
      else if(part == ".frame-editor-nw") this.dragging_mode = "nw-resize";
      else if(part == ".frame-editor-ne") this.dragging_mode = "ne-resize";
      else if(part == ".frame-editor-sw") this.dragging_mode = "sw-resize";
      else if(part == ".frame-editor-se") this.dragging_mode = "se-resize";

      var post = Post.posts.get(this.post_id);
      var frame = post.frames_pending[this.post_frame];
      this.dragging_anchor = frame_dimensions_to_image(frame, this.image_dimensions, post);

      /* When dragging a handle, hide the cursor to get it out of the way. */
      this.dragger.overriden_drag_class = this.dragging_mode == "move"? null: "hide-cursor";

      /* Stop propagation of the event, so any other draggers in the chain don't start.  In
       * particular, when we're dragging inside the image, we need to stop WindowDragElementAbsolute.
       * Only do this if we're actually dragging, not if we aborted due to this.drag_to_create. */
      e.latest_event.stopPropagation();
    }.bind(this),

    ondrag: function(e) {
      var post = Post.posts.get(this.post_id);

      /* Invert the motion, since we're dragging the image around underneith the
       * crop frame instead of dragging the crop frame around. */
      var dims = apply_drag(this.dragging_mode, -e.aX, -e.aY, this.image_dimensions, this.dragging_anchor);

      /* Scale the changed dimensions back to the source resolution and apply them
       * to the frame. */
      var source_dims = frame_dimensions_from_image(dims, this.image_dimensions, post);
      post.frames_pending[this.post_frame] = source_dims;

      if(this.options.onUpdate)
        this.options.onUpdate();
    }.bind(this)
  });

  this.update();
}

/*
 * Set the post to show in the corner dragger.  If post_id is null, clear any displayed
 * post.
 *
 * When the post ID is set, the post frame is always cleared.
 */
CornerDragger.prototype.set_post_id = function(post_id)
{
  this.post_id = post_id;
  this.post_frame = null;

  var url = null;
  var img = this.container.down("img");
  if(post_id != null)
  {
    var post = Post.posts.get(this.post_id);
    this.image_dimensions = {
      width: post.jpeg_width, height: post.jpeg_height
    };

    url = post.jpeg_url;
    img.width = this.image_dimensions.width;
    img.height = this.image_dimensions.height;
  }

  /* Don't change the image if it's already set; it causes Chrome to reprocess the
   * image. */
  if(img.src != url)
  {
    img.src = url;

    if(Prototype.Browser.WebKit && url)
    {
      /* Decoding in Chrome takes long enough to be visible.  Hourglass the cursor while it runs. */
      document.documentElement.addClassName("hourglass");
      (function() { document.documentElement.removeClassName("hourglass"); }.defer());
    }
  }

  this.update();
}

CornerDragger.prototype.set_post_frame = function(post_frame)
{
  this.post_frame = post_frame;

  this.update();
}

CornerDragger.prototype.update = function()
{
  if(this.post_id == null || this.post_frame == null)
    return;

  var post = Post.posts.get(this.post_id);
  var frame = post.frames_pending[this.post_frame];
  var dims = frame_dimensions_to_image(frame, this.image_dimensions, post);

  var div = this.container;

  /* Update the drag/frame box. */
  var box = this.container.down(".frame-editor-frame-box");
  box.style.left = dims.left + "px";
  box.style.top = dims.top + "px";
  box.style.width = dims.width + "px";
  box.style.height = dims.height + "px";

  /* Recenter the corner box. */
  var top = dims.top;
  var left = dims.left;
  if(this.part == ".frame-editor-ne" || this.part == ".frame-editor-se")
    left += dims.width;
  if(this.part == ".frame-editor-sw" || this.part == ".frame-editor-se")
    top += dims.height;

  var offset_height = div.offsetHeight/2;
  var offset_width = div.offsetWidth/2;
  /*
  if(this.part == ".frame-editor-nw" || this.part == ".frame-editor-ne") offset_height -= div.offsetHeight/4;
  if(this.part == ".frame-editor-sw" || this.part == ".frame-editor-se") offset_height += div.offsetHeight/4;
  if(this.part == ".frame-editor-nw" || this.part == ".frame-editor-sw") offset_width -= div.offsetWidth/4;
  if(this.part == ".frame-editor-ne" || this.part == ".frame-editor-se") offset_width += div.offsetWidth/4;
  */
  left -= offset_width;
  top -= offset_height;

  /* If the region is small enough that we don't have enough to fill the corner
   * frames, push the frames inward so they line up. */
  if(this.part == ".frame-editor-nw" || this.part == ".frame-editor-sw")
    left = Math.min(left, dims.left + dims.width/2 - div.offsetWidth);
  if(this.part == ".frame-editor-ne" || this.part == ".frame-editor-se")
    left = Math.max(left, dims.left + dims.width/2);
  if(this.part == ".frame-editor-nw" || this.part == ".frame-editor-ne")
    top = Math.min(top, dims.top + dims.height/2 - div.offsetHeight);
  if(this.part == ".frame-editor-sw" || this.part == ".frame-editor-se")
    top = Math.max(top, dims.top + dims.height/2);

  var img = this.container.down(".frame-editor-popup-div");
  img.style.marginTop = (-top) + "px";
  img.style.marginLeft = (-left) + "px";
}