/*
 * We have a few competing goals:
 *
 * First, be as responsive as possible.  Preload nearby post HTML and their images.
 *
 * If data in a post page changes, eg. if the user votes, then coming back to the page
 * later should retain the changes.  This means either requesting the page again, or
 * retaining the document node and reusing it, so we preserve the changes that were
 * made in-place.
 *
 * Don't use too much memory.  If we keep every document node in memory as we use it,
 * the images will probably be kept around too.  Release older nodes, so the browser
 * is more likely to release images that havn't been used in a while.
 *
 * We do the following:
 * - When we load a new post, it's formatted and its scripts are evaluated normally.
 * - When we're replacing the displayed post, its node is stashed away in a node cache.
 * - If we come back to the post while it's in the node cache, we'll use the node directly.
 * - HTML and images for posts are preloaded.  We don't use a simple mechanism like
 *   Preload.preload_raw, because Opera's caching is broken for XHR and it'll always
 *   do a slow revalidation.
 * - We don't depend on browser caching for HTML.  That would require us to expire a
 *   page when we switch away from it if we've made any changes (eg. voting), so we
 *   don't pull an out-of-date page next time.  This is slower, and would require us
 *   to be careful about expiring the cache.
 */

BrowserView = function(container)
{
  this.container = container;

  /* The post that we currently want to display.  This will be either one of the
   * current html_preloads, or be the displayed_post_id. */
  this.wanted_post_id = null;
  this.wanted_post_frame = null;

  /* The post that's currently actually being displayed. */
  this.displayed_post_id = null;
  this.displayed_post_frame = null;

  this.current_ajax_request = null;
  this.last_preload_request = [];
  this.last_preload_request_active = false;

  this.image_pool = new ImgPoolHandler();
  this.img_box = this.container.down(".image-box");
  this.container.down(".image-canvas");
  
  /* Pop. Show large by default {
   */
  (function($, B){
    /* MI: If mobile browser, do nothing. */
    if (Prototype.BrowserIsMobile)
      return;
    
    var COOKIE_NAME = "browse_large_by_default";
    B.large_by_default = jQuery.cookie(COOKIE_NAME) === "0" ? false : true;
    
    var li = $($('.post-info-right-edge li')[0]).css("position", "relative");
    var div = $('.default_to_large_cont'), a = div.children('a'), s = a.children('span');
    function toggle_show_large() {
      B.large_by_default = !B.large_by_default;
      (B.large_by_default ? s.show() : s.hide());
      jQuery.cookie(COOKIE_NAME, (B.large_by_default ? 1 : 0));
    }
    
    a.click(function(){
      toggle_show_large();
      return false;
    });
    
    li.hover(function(){
      div.show();
    }, function(){
      div.hide();
    });
    
    if (!B.large_by_default)
      s.hide();
  })(jQuery, this);
  /* } */
  
  /* In Opera 10.63, the img.complete property is not reset to false after changing the
   * src property.  Blits from images to the canvas silently fail, with nothing being
   * blitted and no exception raised.  This causes blank canvases to be displayed, because
   * we have no way of telling whether the image is blittable or if the blit succeeded. */
  if(!Prototype.Browser.Opera)
    this.canvas = create_canvas_2d();
  if(this.canvas)
  {
    this.canvas.hide();
    this.img_box.appendChild(this.canvas);
  }
  this.zoom_level = 0;

  /* True if the post UI is visible. */
  this.post_ui_visible = true;

  this.update_navigator = this.update_navigator.bind(this);

  Event.on(window, "resize", this.window_resize_event.bindAsEventListener(this));
  document.on("viewer:vote", function(event) { if(this.vote_widget) this.vote_widget.set(event.memo.score); }.bindAsEventListener(this));

  if(TagCompletion)
    TagCompletion.init();

  /* Double-clicking the main image, or on nothing, toggles the thumb bar. */
  this.container.down(".image-container").on("dblclick", ".image-container", function(event) {
    /* Watch out: Firefox fires dblclick events for all buttons, with the standard
     * button maps, but IE only fires it for left click and doesn't set button at
     * all, so event.isLeftClick won't work. */
    if(event.button)
      return;

    event.stop();
    document.fire("viewer:set-thumb-bar", {toggle: true});
  }.bindAsEventListener(this));

  /* Image controls: */
  document.on("viewer:view-large-toggle", function(e) { this.toggle_view_large_image(); }.bindAsEventListener(this));
  this.container.down(".post-info").on("click", ".toggle-zoom", function(e) { e.stop(); this.toggle_view_large_image(true); }.bindAsEventListener(this)); // Pop.
  this.container.down(".parent-post").down("A").on("click", this.parent_post_click_event.bindAsEventListener(this));
  this.container.down(".child-posts").down("A").on("click", this.child_posts_click_event.bindAsEventListener(this));

  this.container.down(".post-frames").on("click", ".post-frame-link", function(e, item) {
    e.stop();

    /* Change the displayed post frame to the one that was clicked.  Since all post frames
     * are usually displayed in the thumbnail view, set center_thumbs to true to recenter
     * on the thumb that was clicked, so it's clearer what's happening. */
    document.fire("viewer:set-active-post", {post_id: this.displayed_post_id, post_frame: item.post_frame, center_thumbs: true});
  }.bind(this));

  /* We'll receive this message from the thumbnail view when the overlay is
   * visible on the bottom of the screen, to tell us how much space is covered up
   * by it. */
  this.thumb_bar_height = 0;
  document.on("viewer:thumb-bar-changed", function(e) {
    /* Update the thumb bar height and rescale the image to fit the new area. */
    this.thumb_bar_height = e.memo.height;
    this.update_image_window_size();

    this.set_post_ui(e.memo.shown);
    this.scale_and_position_image(true);
  }.bindAsEventListener(this));

/*
  OnKey(79, null, function(e) {
    this.zoom_level -= 1;
    this.scale_and_position_image(true);
    this.update_navigator();
    return true;
  }.bindAsEventListener(this));

  OnKey(80, null, function(e) {
    this.zoom_level += 1;
    this.scale_and_position_image(true);
    this.update_navigator();
    return true;
  }.bindAsEventListener(this));
*/
  /* Hide member-only and moderator-only controls: */
  $(document.body).pickClassName("is-member", "not-member", User.is_member_or_higher());
  $(document.body).pickClassName("is-moderator", "not-moderator", User.is_mod_or_higher());

  var tag_span = this.container.down(".post-tags");
  tag_span.on("click", ".post-tag", function(e, element) {
    e.stop();
    document.fire("viewer:perform-search", {tags: element.tag_name});
  }.bind(this));

  /* These two links do the same thing, but one is shown to approve a pending post
   * and the other is shown to unflag a flagged post, so they prompt differently. */
  this.container.down(".post-approve").on("click", function(e) {
    e.stop();
    if(!confirm("Approve this post?"))
      return;
    var post_id = this.displayed_post_id;
    Post.approve(post_id, false);
  }.bindAsEventListener(this));

  this.container.down(".post-unflag").on("click", function(e) {
    e.stop();
    if(!confirm("Unflag this post?"))
      return;
    var post_id = this.displayed_post_id;
    Post.unflag(post_id);
  }.bindAsEventListener(this));

  this.container.down(".post-delete").on("click", function(e) {
    e.stop();
    var post = Post.posts.get(this.displayed_post_id);
    var default_reason = "";
    if(post.flag_detail)
      default_reason = post.flag_detail.reason;

    var reason = prompt("Reason:", default_reason);
    if(!reason || reason == "")
      return;
    var post_id = this.displayed_post_id;
    Post.approve(post_id, reason);
  }.bindAsEventListener(this));

  this.container.down(".post-undelete").on("click", function(e) {
    e.stop();
    if(!confirm("Undelete this post?"))
      return;
    var post_id = this.displayed_post_id;
    Post.undelete(post_id);
  }.bindAsEventListener(this));

  this.container.down(".flag-button").on("click", function(e) {
    e.stop();
    var post_id = this.displayed_post_id;
    Post.flag(post_id);
  }.bindAsEventListener(this));

  this.container.down(".activate-post").on("click", function(e) {
    e.stop();

    var post_id = this.displayed_post_id;
    if(!confirm("Activate this post?"))
      return;
    Post.update_batch([{ id: post_id, is_held: false }], function()
    {
      var post = Post.posts.get(post_id);
      if(post.is_held)
        notice("Couldn't activate post");
      else
        notice("Activated post");
    }.bind(this));
  }.bindAsEventListener(this));

  this.container.down(".reparent-post").on("click", function(e) {
    e.stop();

    if(!confirm("Make this post the parent?"))
      return;

    var post_id = this.displayed_post_id;
    var post = Post.posts.get(post_id);
    if(post == null)
      return;

    Post.reparent_post(post_id, post.parent_id, false);
  }.bindAsEventListener(this));

  this.container.down(".pool-info").on("click", ".remove-pool-from-post", function(e, element)
  {
    e.stop();
    var pool_info = element.up(".pool-info");
    var pool = Pool.pools.get(pool_info.pool_id);
    var pool_name = pool.name.replace(/_/g, ' ');
    if(!confirm("Remove this post from pool #" + pool_info.pool_id + ": " + pool_name + "?"))
      return;

    Pool.remove_post(pool_info.post_id, pool_info.pool_id);
  }.bind(this));

  /* Post editing: */
  var post_edit = this.container.down(".post-edit");
  post_edit.down("FORM").on("submit", function(e) { e.stop(); this.edit_save(); }.bindAsEventListener(this));
  this.container.down(".show-tag-edit").on("click", function(e) { e.stop(); this.edit_show(true); }.bindAsEventListener(this));
  this.container.down(".edit-save").on("click", function(e) { e.stop(); this.edit_save(); }.bindAsEventListener(this));
  this.container.down(".edit-cancel").on("click", function(e) { e.stop(); this.edit_show(false); }.bindAsEventListener(this));

  this.edit_post_area_changed = this.edit_post_area_changed.bind(this);
  post_edit.down(".edit-tags").on("paste", function(e) { this.edit_post_area_changed.defer(); }.bindAsEventListener(this));
  post_edit.down(".edit-tags").on("keydown", function(e) { this.edit_post_area_changed.defer(); }.bindAsEventListener(this));
  new TagCompletionBox(post_edit.down(".edit-tags"));

  this.container.down(".post-edit").on("keydown", function(e) {
    /* Don't e.stop() KEY_ESC, so we fall through and let handle_keypress unfocus the
     * form entry, if any.  Otherwise, Chrome gets confused and leaves the focus on the
     * hidden input, where it'll steal keystrokes. */
    if (e.keyCode == Event.KEY_ESC) { this.edit_show(false); }
    else if (e.keyCode == Event.KEY_RETURN) { e.stop(); this.edit_save(); }
  }.bindAsEventListener(this));

  /* When the edit-post hotkey is pressed (E), force the post UI open and show editing. */
  document.on("viewer:edit-post", function(e) {
    document.fire("viewer:set-thumb-bar", { set: true });
    this.edit_show(true);
  }.bindAsEventListener(this));

  /* When the post that's currently being displayed is updated by an API call, update
   * the displayed info. */
  document.on("posts:update", function(e) {
    if(e.memo.post_ids.get(this.displayed_post_id) == null)
      return;
    this.set_post_info();
  }.bindAsEventListener(this));

  this.vote_widget = new Vote(jQuery(this.container.down(".vote-container"), null));
  this.vote_widget.initShortcut();

  this.blacklist_override_post_id = null;
  this.container.down(".show-blacklisted").on("click", function(e) { e.preventDefault(); }.bindAsEventListener(this));
  this.container.down(".show-blacklisted").on("dblclick", function(e) {
    e.stop();
    this.blacklist_override_post_id = this.displayed_post_id;
    var post = Post.posts.get(this.displayed_post_id);
    this.set_main_image(post, this.displayed_post_frame);
  }.bindAsEventListener(this));


  this.img_box.on("viewer:center-on", function(e) { this.center_image_on(e.memo.x, e.memo.y); }.bindAsEventListener(this));

  this.navigator = new Navigator(this.container.down(".image-navigator"), this.img_box);

  this.container.on("swipe:horizontal", function(e) { document.fire("viewer:show-next-post", { prev: e.memo.right }); }.bindAsEventListener(this));

  // if(Prototype.BrowserFeatures.Touchscreen)
  /* MI: Using Prototype.BrowserIsMobile instead. */
  if (Prototype.BrowserIsMobile)
  {
    this.create_voting_popup();
    this.image_swipe = new SwipeHandler(this.container.down(".image-container"));
  }

  /* Create the frame editor.  This must be created before image_dragger, since it takes priority
   * for drags. */
  this.container.down(".edit-frames-button").on("click", function(e) { e.stop(); this.show_frame_editor(); }.bindAsEventListener(this));
  this.frame_editor = new FrameEditor(this.container.down(".frame-editor"), this.img_box, this.container.down(".frame-editor-popup"),
  {
    onClose: function() {
      this.hide_frame_editor();
    }.bind(this)
  });

  /* If we're using dragging as a swipe gesture (see SwipeHandler), don't use it for
   * dragging too. */
  if(this.image_swipe == null)
    this.image_dragger = new WindowDragElementAbsolute(this.img_box, this.update_navigator);
}

BrowserView.prototype.create_voting_popup = function()
{
  /* Create the low-level voting widget. */
  var popup_vote_widget_container = this.container.down(".vote-popup-container");
  popup_vote_widget_container.show();
  this.popup_vote_widget = new Vote(jQuery(popup_vote_widget_container), null);
  this.popup_vote_widget.initShortcut();

  var flash = this.container.down(".vote-popup-flash");

  /* vote-popup-expand is the part that's always present and is clicked to display the
   * voting popup.  Create a dragger on it, and pass the position down to the voting
   * popup as we drag around. */
  var popup_expand = this.container.down(".vote-popup-expand");
  popup_expand.show();

  var last_dragged_over = null;

  this.popup_vote_dragger = new DragElement(popup_expand, {
    ondown: function(drag) {
      /* Stop the touchdown/mousedown events, so this drag takes priority over any
       * others.  In particular, we don't want this.image_swipe to also catch this
       * as a drag. */
      drag.latest_event.stop();

      flash.hide();
      flash.removeClassName("flash-star");

      this.popup_vote_widget.set_mouseover(null);
      last_dragged_over = null;
      popup_vote_widget_container.removeClassName("vote-popup-hidden");
    }.bind(this),

    onup: function(drag) {
      /* If we're cancelling the drag, don't activate the vote, if any. */
      if(drag.cancelling)
      {
        debug("cancelling drag");
        last_dragged_over = null;
      }

      /* Call even if star_container is null or not a star, so we clear any mouseover. */
      this.popup_vote_widget.set_mouseover(last_dragged_over);

      var star = this.popup_vote_widget.activate_item(last_dragged_over);

      /* If a vote was made, flash the vote star. */
      if(star != null)
      {
        /* Set the star-# class to color the star. */
        for(var i = 0; i < 4; ++i)
          flash.removeClassName("star-" + i);
        flash.addClassName("star-" + star);

        flash.show();

        /* Center the element on the screen. */
        var offset = this.image_window_size;
        var flash_x = offset.width/2 - flash.offsetWidth/2;
        var flash_y = offset.height/2 - flash.offsetHeight/2;
        flash.setStyle({left: flash_x + "px", top: flash_y + "px"});
        flash.addClassName("flash-star");
      }

      popup_vote_widget_container.addClassName("vote-popup-hidden");
      last_dragged_over = null;
    }.bind(this),

    ondrag: function(drag) {
      last_dragged_over = document.elementFromPoint(drag.x, drag.y);
      this.popup_vote_widget.set_mouseover(last_dragged_over);
    }.bind(this)
  });
}


BrowserView.prototype.set_post_ui = function(visible)
{
  /* Disable the post UI by default on touchscreens; we don't have an interface
   * to toggle it. */
  // if(Prototype.BrowserFeatures.Touchscreen)
  /* MI: Using Prototype.BrowserIsMobile instead. */
  if (Prototype.BrowserIsMobile)
    visible = false;

  /* If we don't have a post displayed, always hide the post UI even if it's currently
   * shown. */
  this.container.down(".post-info").show(visible && this.displayed_post_id != null);

  if(visible == this.post_ui_visible)
    return;

  this.post_ui_visible = visible;
  if(this.navigator)
    this.navigator.set_autohide(!visible);

  /* If we're hiding the post UI, cancel the post editor if it's open. */
  if(!this.post_ui_visible)
    this.edit_show(false);
}


BrowserView.prototype.image_loaded_event = function(event)
{
  /* Record that the image is completely available, so it can be blitted to the canvas.
   * This is different than img.complete, which is true if the image has completed downloading
   * but hasn't yet been decoded, so isn't yet completely available.  This generally happens
   * if we query img.completed quickly after setting img.src and the image data is cached. */
  this.img.fully_loaded = true;

  document.fire("viewer:displayed-image-loaded", { post_id: this.displayed_post_id, post_frame: this.displayed_post_frame });
  this.update_canvas();
}

/* Return true if last_preload_request includes [post_id, post_frame]. */
BrowserView.prototype.post_frame_list_includes = function(post_id_list, post_id, post_frame)
{
  var found_preload = post_id_list.find(function(post) { return post[0] == post_id && post[1] == post_frame; });
  return found_preload != null;
}

/* Begin preloading the HTML and images for the given post IDs. */
BrowserView.prototype.preload = function(post_ids)
{
  /* We're being asked to preload post_ids.  Only do this if it seems to make sense: if
   * the user is actually traversing posts that are being preloaded.  Look at the previous
   * call to preload().  If it didn't include the current post, then skip the preload. */
  var last_preload_request = this.last_preload_request;
  this.last_preload_request = post_ids;

  if(!this.post_frame_list_includes(last_preload_request, this.wanted_post_id, this.wanted_post_frame))
  {
    // debug("skipped-preload(" + post_ids.join(",") + ")");
    this.last_preload_request_active = false;
    return;
  }
  this.last_preload_request_active = true;
  // debug("preload(" + post_ids.join(",") + ")");

  var new_preload_container = new PreloadContainer();
  for(var i = 0; i < post_ids.length; ++i)
  {
    var post_id = post_ids[i][0];
    var post_frame = post_ids[i][1];
    var post = Post.posts.get(post_id);

    if(post_frame != -1)
    {
      var frame = post.frames[post_frame];
      new_preload_container.preload(frame.url);
    }
    else
      new_preload_container.preload(post.sample_url);
  }

  /* If we already were preloading images, we created the new preloads before
   * deleting the old ones.  That way, any images that are still being preloaded
   * won't be deleted and recreated, possibly causing the download to be interrupted
   * and resumed. */
  if(this.preload_container)
    this.preload_container.destroy();
  this.preload_container = new_preload_container;
}

BrowserView.prototype.load_post_id_data = function(post_id)
{
  debug("load needed");

  // If we already have a request in flight, don't start another; wait for the
  // first to finish.
  if(this.current_ajax_request != null)
    return;

  new Ajax.Request("/post.json", {
    parameters: {
      tags: "id:" + post_id,
      api_version: 2,
      filter: 1,
      include_tags: "1",
      include_votes: "1",
      include_pools: 1
    },
    method: "get",

    onCreate: function(resp) {
      this.current_ajax_request = resp.request;
    }.bind(this),

    onSuccess: function(resp) {
      if(this.current_ajax_request != resp.request)
        return;

      /* If no posts were returned, then the post ID we're looking up doesn't exist;
       * treat this as a failure. */
      var resp = resp.responseJSON;
      this.success = resp.posts.length > 0;
      if(!this.success)
      {
        notice("Post #" + post_id + " doesn't exist");
        return;
      }

      Post.register_resp(resp);
    }.bind(this),

    onComplete: function(resp) {
      if(this.current_ajax_request == resp.request)
        this.current_ajax_request = null;

      /* If the request failed and we were requesting wanted_post_id, don't keep trying. */
      var success = resp.request.success() && this.success;
      if(!success && post_id == this.wanted_post_id)
      {
        /* As a special case, if the post we requested doesn't exist and we aren't displaying
         * anything at all, force the thumb bar open so we don't show nothing at all. */
        if(this.displayed_post_id == null)
          document.fire("viewer:set-thumb-bar", {set: true});

        return;
      }

      /* This will either load the post we just finished, or request data for the
       * one we want. */
      this.set_post(this.wanted_post_id, this.wanted_post_frame);
    }.bind(this),

    onFailure: function(resp) {
      notice("Error " + resp.status + " loading post");
    }.bind(this)
  });
}

BrowserView.prototype.set_viewing_larger_version = function(b, from_toggle_zoom) // Pop.
{
  this.viewing_larger_version = b;
  
  var post = Post.posts.get(this.displayed_post_id);
  var can_zoom = post != null && post.jpeg_url != post.sample_url;
  
  var viewing_larger_version = from_toggle_zoom || !this.large_by_default ? b : !b; // Pop.
  
  this.container.down(".zoom-icon-none").show(!can_zoom);
  this.container.down(".zoom-icon-in").show(can_zoom && !viewing_larger_version); // Pop.
  this.container.down(".zoom-icon-out").show(can_zoom && viewing_larger_version); // Pop.

  /* When we're on the regular version and we're on a touchscreen, disable drag
   * scrolling so we can use it to switch images instead. */
  // if(Prototype.BrowserFeatures.Touchscreen && this.image_dragger)
  /* MI: Using Prototype.BrowserIsMobile instead. */
  if (Prototype.BrowserIsMobile && this.image_dragger)
    this.image_dragger.set_disabled(!b);

  /* Only allow dragging to create new frames when not viewing the large version,
   * since we need to be able to drag the image. */
  if(this.frame_editor)
  {
    this.frame_editor.set_drag_to_create(!b);
    this.frame_editor.set_show_corner_drag(!b);
  }
}

BrowserView.prototype.set_main_image = function(post, post_frame, from_toggle_zoom) // Pop.
{
  /*
   * Clear the previous post, if any.  Don't keep the old IMG around; create a new one, or
   * we may trigger long-standing memory leaks in WebKit, eg.:
   * https://bugs.webkit.org/show_bug.cgi?id=31253
   *
   * This also helps us avoid briefly displaying the old image with the new dimensions, which
   * can otherwise take some hoop jumping to prevent.
   */
  if(this.img != null)
  {
    this.img.stopObserving();
    this.img.parentNode.removeChild(this.img);
    this.image_pool.release(this.img);
    this.img = null;
  }

  /* If this post is blacklisted, show a message instead of displaying it. */
  var hide_post = Post.is_blacklisted(post.id) && post.id != this.blacklist_override_post_id;
  this.container.down(".blacklisted-message").show(hide_post);
  if(hide_post)
    return;

  this.img = this.image_pool.get();
  this.img.className = "main-image";

  if(this.canvas)
    this.canvas.hide();
  this.img.show();

  /*
   * Work around an iPhone bug.  If a touchstart event is sent to this.img, and then
   * (due to a swipe gesture) we remove the image and replace it with a new one, no
   * touchend is ever delivered, even though it's the containing box listening to the
   * event.  Work around this by setting the image to pointer-events: none, so clicks on
   * the image will actually be sent to the containing box directly.
   */
  this.img.setStyle({pointerEvents: "none"});

  this.img.on("load", this.image_loaded_event.bindAsEventListener(this));

  if (!from_toggle_zoom && this.large_by_default)
    this.viewing_larger_version = true
  
  this.img.fully_loaded = false;
  if(post_frame != -1 && post_frame < post.frames.length)
  {
    var frame = post.frames[post_frame];
    this.img.src = frame.url;
    this.img_box.original_width = frame.width;
    this.img_box.original_height = frame.height;
    this.img_box.show();
  }
  else if(this.viewing_larger_version && post.jpeg_url)
  {
    this.img.src = post.jpeg_url;
    this.img_box.original_width = post.jpeg_width;
    this.img_box.original_height = post.jpeg_height;
    this.img_box.show();
  }
  else if(!this.viewing_larger_version && post.sample_url)
  {
    this.img.src = post.sample_url;
    this.img_box.original_width = post.sample_width;
    this.img_box.original_height = post.sample_height;
    this.img_box.show();
  }
  else
  {
    /* Having no sample URL is an edge case, usually from deleted posts.  Keep the number
     * of code paths smaller by creating the IMG anyway, but not showing it. */
    this.img_box.hide();
  }

  this.container.down(".image-box").appendChild(this.img);

  if(this.viewing_larger_version)
  {
    this.navigator.set_image(post.preview_url, post.actual_preview_width, post.actual_preview_height);
    this.navigator.set_autohide(!this.post_ui_visible);
  }
  this.navigator.enable(this.viewing_larger_version);

  this.scale_and_position_image();
  
}

/*
 * Display post_id.  If post_frame is not null, set the specified frame.
 *
 * If no_hash_change is true, the UrlHash will not be updated to reflect the new post.
 * This should be used when this is called to load the post already reflected by the
 * URL hash.  For example, the hash "#/pool:123" shows pool 123 in the thumbnails and
 * shows its first post in the view.  It should *not* change the URL hash to reflect
 * the actual first post (eg. #12345/pool:123).  This will insert an unwanted history
 * state in the browser, so the user has to go back twice to get out.
 *
 * no_hash_change should also be set when loading a state as a result of hashchange,
 * for similar reasons.
 */
BrowserView.prototype.set_post = function(post_id, post_frame, lazy, no_hash_change, replace_history)
{
  if(post_id == null)
    throw "post_id must not be null";

  /* If there was a lazy load pending, cancel it. */
  this.cancel_lazily_load();

  this.wanted_post_id = post_id;
  this.wanted_post_frame = post_frame;
  this.wanted_post_no_hash_change = no_hash_change;
  this.wanted_post_replace_history = replace_history;

  if(post_id == this.displayed_post_id && post_frame == this.displayed_post_frame)
    return;

  /* If a lazy load was requested and we're not yet loading the image for this post,
   * delay loading. */
  var is_cached = this.last_preload_request_active && this.post_frame_list_includes(this.last_preload_request, post_id, post_frame);
  if(lazy && !is_cached)
  {
    this.lazy_load_timer = window.setTimeout(function() {
      this.lazy_load_timer = null;
      this.set_post(this.wanted_post_id, this.wanted_post_frame, false, this.wanted_post_no_hash_change, this.wanted_post_replace_history);
    }.bind(this), 500);
    return;
  }

  this.hide_frame_editor();

  var post = Post.posts.get(post_id);
  if(post == null)
  {
    /* The post we've been asked to display isn't loaded.  Request a load and come back. */
    if(this.displayed_post_id == null)
      this.container.down(".post-info").hide();

    this.load_post_id_data(post_id);
    return;
  }

  if(post_frame == null) {
    // If post_frame is unspecified and we have a frame, display the first.
    post_frame = this.get_default_post_frame(post_id);

    // We know what frame we actually want to display now, so update wanted_post_frame.
    this.wanted_post_frame = post_frame;
  }

  /* If post_frame doesn't exist, just display the main post. */
  if(post_frame != -1 && post.frames.length <= post_frame)
    post_frame = -1;

  this.displayed_post_id = post_id;
  this.displayed_post_frame = post_frame;
  if(!no_hash_change) {
    var post_frame_hash = this.get_post_frame_hash(post, post_frame);
    UrlHash.set_deferred({"post-id": post_id, "post-frame": post_frame_hash}, replace_history);
  }

  this.set_viewing_larger_version(false);

  this.set_main_image(post, post_frame);

  if(this.vote_widget) {
    if (this.vote_widget.post_id) {
      Post.votes.set(this.vote_widget.post_id, this.vote_widget.data.vote);
      Post.posts.get(this.vote_widget.post_id).score = this.vote_widget.data.score;
    }
    this.vote_widget.post_id = post.id;
    this.vote_widget.updateWidget(Post.votes.get(post.id), post.score);
  };
  if(this.popup_vote_widget) {
    this.popup_vote_widget.post_id = post.id;
    this.popup_vote_widget.updateWidget(Post.votes.get(post.id), post.score);
  };

  document.fire("viewer:displayed-post-changed", { post_id: post_id, post_frame: post_frame });

  this.set_post_info();

  /* Hide the editor when changing posts. */
  this.edit_show(false);
}

/* Return the frame spec for the hash, eg. "-0".
 *
 * If the post has no frames, then just omit the frame spec.  If the post has any frames,
 * then return the frame number or "-F" for the full image. */
BrowserView.prototype.post_frame_hash = function(post, post_frame)
{
  if(post.frames.length == 0)
    return "";
  return "-" + (post_frame == -1? "F":post_frame);
}

/* Return the default frame to display for the given post.  If the post isn't loaded,
 * we don't know which frame we'll display and null will be returned.  This corresponds
 * to a hash of #1234, where no frame is specified (eg. #1234-F, #1234-0). */
BrowserView.prototype.get_default_post_frame = function(post_id)
{
  var post = Post.posts.get(post_id);
  if(post == null)
    return null;

  return post.frames.length > 0? 0: -1;
}

BrowserView.prototype.get_post_frame_hash = function(post, post_frame)
{
/*
 * Omitting the frame in the hash selects the default frame: the first frame if any,
 * otherwise the full image.  If we're setting the hash to a post_frame which would be
 * selected by this default, omit the frame so this default is used.  For example, if
 * post #1234 has one frame and post_frame is 0, it would be selected by the default,
 * so omit the frame and use a hash of #1234, not #1234-0.
 *
 * This helps normalize the hash.  Otherwise, loading /#1234 will update the hash to
 * /#1234-in set_post, causing an unwanted history entry.
 */
  var default_frame = post.frames.length > 0? 0:-1;
  if(post_frame == default_frame)
    return null;
  else
    return post_frame;
}
/* Set the post info box for the currently displayed post. */
BrowserView.prototype.set_post_info = function()
{
  var post = Post.posts.get(this.displayed_post_id);
  if(!post)
    return;

  this.container.down(".post-id").setTextContent(post.id);
  this.container.down(".post-id-link").href = "/post/show/" + post.id;
  this.container.down(".posted-by").show();
  this.container.down(".posted-at").setTextContent(time_ago_in_words(new Date(post.created_at*1000)));

  /* Fill in the pool list. */
  var pool_info = this.container.down(".pool-info");
  while(pool_info.firstChild)
    pool_info.removeChild(pool_info.firstChild);
  if(post.pool_posts)
  {
    post.pool_posts.each(function(pp) {
      var pool_post = pp[1];
      var pool_id = pool_post.pool_id;
      var pool = Pool.pools.get(pool_id);

      var pool_title = pool.name.replace(/_/g, " ");
      var sequence = pool_post.sequence;
      if(sequence.match(/^[0-9]/))
        sequence = "#" + sequence;

      var html =
        '<div class="pool-info">Post ${sequence} in <a class="pool-link" href="/post/browse#/pool:${pool_id}">${desc}</a> ' +
        '(<a target="_blank" href="/pool/show/${pool_id}">pool page</a>)';

      if(Pool.can_edit_pool(pool))
        html += '<span class="advanced-editing"> (<a href="#" class="remove-pool-from-post">remove</a>)</div></span>';

      var div = html.subst({
        sequence: sequence,
        pool_id: pool_id,
        desc: pool_title.escapeHTML()
      }).createElement();

      div.post_id = post.id;
      div.pool_id = pool_id;

      pool_info.appendChild(div);
    }.bind(this));
  }

  if(post.creator_id != null)
  {
    this.container.down(".posted-by").down("A").href = "/user/show/" + post.creator_id;
    this.container.down(".posted-by").down("A").setTextContent(post.author);
  } else {
    this.container.down(".posted-by").down("A").href = "#"
    this.container.down(".posted-by").down("A").setTextContent('Anonymous');
  }

  this.container.down(".post-dimensions").setTextContent(post.width + "x" + post.height);
  this.container.down(".post-source").show(post.source != "");
  if(post.source != "")
  {
    var text = post.source;
    var url = null;

    var m = post.source.match(/^http:\/\/.*pixiv\.net\/(img\d+\/)?img\/(\w+)\/(\d+)(_.+)?\.\w+$/);
    if(m)
    {
      text = "pixiv #" + m[3] + " (" + m[2] + ")";
      url = "http://www.pixiv.net/member_illust.php?mode=medium&illust_id=" + m[3];
    }
    else if(post.source.substr(0, 7) == "http://")
    {
      text = text.substr(7);
      if(text.substr(0, 4) == "www.")
        text = text.substr(4);
      if(text.length > 20)
        text = text.substr(0, 20) + "...";
      url = post.source;
    }

    var source_box = this.container.down(".post-source");

    source_box.down("A").show(url != null);
    source_box.down("SPAN").show(url == null);
    if(url)
    {
      source_box.down("A").href = url;
      source_box.down("A").setTextContent(text);
    }
    else
    {
      source_box.down("SPAN").setTextContent(text);
    }

  }

  if(post.frames.length > 0)
  {
    /* Hide this with a class rather than by changing display, so show_frame_editor
     * and hide_frame_editor can hide and unhide this separately. */
    this.container.down(".post-frames").removeClassName("no-frames");

    var frame_list = this.container.down(".post-frame-list");
    while(frame_list.firstChild)
      frame_list.removeChild(frame_list.firstChild);

    for(var i = -1; i < post.frames.length; ++i)
    {
      var text = i == -1? "main": (i+1);

      var a = document.createElement("a");
      a.href = "/post/browse#" + post.id  + this.post_frame_hash(post, i);

      a.className = "post-frame-link";
      if(this.displayed_post_frame == i)
        a.className += " current-post-frame";

      a.setTextContent(text);
      a.post_frame = i;
      frame_list.appendChild(a);
    }
  }
  else
  {
    this.container.down(".post-frames").addClassName("no-frames");
  }


  var ratings = {s: "Safe", q: "Questionable", e: "Explicit"};
  this.container.down(".post-rating").setTextContent(ratings[post.rating]);
  this.container.down(".post-score").setTextContent(post.score);
  this.container.down(".post-hidden").show(!post.is_shown_in_index);

  this.container.down(".post-info").show(this.post_ui_visible);

  var file_extension = function(path)
  {
    var m = path.match(/.*\.([^.]+)/);
    if(!m)
      return "";
    return m[1];
  }

  var has_sample = (post.sample_url != post.file_url);
  var has_jpeg = (post.jpeg_url != post.file_url);
  var has_image = post.file_url != null && !has_sample;

  /* Hide the whole download-links box if there are no downloads available, usually
   * because of a deleted post. */
  this.container.down(".download-links").show(has_image || has_sample || has_jpeg);

  this.container.down(".download-image").show(has_image);
  if(has_image)
  {
    this.container.down(".download-image").href = post.file_url;
    this.container.down(".download-image-desc").setTextContent(number_to_human_size(post.file_size) + " " + file_extension(post.file_url.toUpperCase()));
  }

  this.container.down(".download-jpeg").show(has_sample);
  if(has_sample)
  {
    this.container.down(".download-jpeg").href = has_jpeg? post.jpeg_url: post.file_url;
    var image_desc = number_to_human_size(has_jpeg? post.jpeg_file_size: post.file_size) /*+ " " + post.jpeg_width + "x" + post.jpeg_height*/ + " JPG";
    this.container.down(".download-jpeg-desc").setTextContent(image_desc);
  }

  this.container.down(".download-png").show(has_jpeg);
  if(has_jpeg)
  {
    this.container.down(".download-png").href = post.file_url;
    var png_desc = number_to_human_size(post.file_size) /*+ " " + post.width + "x" + post.height*/ + " " + file_extension(post.file_url.toUpperCase());
    this.container.down(".download-png-desc").setTextContent(png_desc);
  }

  /* For links that are handled by click events, try to set the href so that copying the
   * link will give a similar effect.  For example, clicking parent-post will call set_post
   * to display it, and the href links to /post/browse#12345. */
  var parent_post = this.container.down(".parent-post");
  parent_post.show(post.parent_id != null);
  if(post.parent_id)
    parent_post.down("A").href = "/post/browse#" + post.parent_id;

  var child_posts = this.container.down(".child-posts");
  child_posts.show(post.has_children);
  if(post.has_children)
    child_posts.down("A").href = "/post/browse#/parent:" + post.id;


  /* Create the tag links. */
  var tag_span = this.container.down(".post-tags");
  var first = true;
  while(tag_span.firstChild)
    tag_span.removeChild(tag_span.firstChild);


  var tags_by_type = Post.get_post_tags_with_type(post);
  tags_by_type.each(function(t) {
      var tag = t[0];
      var type = t[1];

      var span = $(document.createElement("SPAN", ""));
      span = $(span);
      span.className = "tag-type-" + type;

      var space = document.createTextNode(" ");
      span.appendChild(space);

      var a =
        jQuery('<a>', {
          text: tag,
          href: "/post/browse#/" + tag,
          class: "post-tag tag-type-" + type,
        });
      /* Break tags with <wbr>, so long tags can be wrapped. */
      a.html(a.html().replace(/_/g, '_<wbr>'));
      /* convert back to something Prototype or whatever can understand */
      a = a[0];
      a.tag_name = tag;
      span.appendChild(a);
      tag_span.appendChild(span);
  });

  var flag_post = this.container.down(".flag-button");
  flag_post.show(post.status == "active");

  this.container.down(".post-approve").show(post.status == "flagged" || post.status == "pending");
  this.container.down(".post-delete").show(post.status != "deleted");
  this.container.down(".post-undelete").show(post.status == "deleted");

  var flagged = this.container.down(".flagged-info");
  flagged.show(post.status == "flagged");
  if(post.status == "flagged" && post.flag_detail)
  {
    var by = flagged.down(".by");
    flagged.down(".flagged-by-box").show(post.flag_detail.user_id != null);
    if(post.flag_detail.user_id != null)
    {
      by.setTextContent(post.flag_detail.flagged_by);
      by.href = "/user/show/" + post.flag_detail.user_id;
    }

    var reason = flagged.down(".reason");
    reason.setTextContent(post.flag_detail.reason);
  }

  /* Moderators can unflag images, and the person who flags an image can unflag it himself. */
  var is_flagger = post.flag_detail && post.flag_detail.user_id == User.get_current_user_id();
  var can_unflag = flagged && (User.is_mod_or_higher() || is_flagger);
  flagged.down(".post-unflag").show(can_unflag);

  var pending = this.container.down(".status-pending");
  pending.show(post.status == "pending");
  this.container.down(".pending-reason-box").show(post.flag_detail && post.flag_detail.reason);
  if(post.flag_detail)
    this.container.down(".pending-reason").setTextContent(post.flag_detail.reason);

  var deleted = this.container.down(".status-deleted");
  deleted.show(post.status == "deleted");
  if(post.status == "deleted")
  {
    var by_container = deleted.down(".by-container");
    by_container.show(post.flag_detail.flagged_by != null);

    var by = by_container.down(".by");
    by.setTextContent(post.flag_detail.flagged_by);
    by.href = "/user/show/" + post.flag_detail.user_id;

    var reason = deleted.down(".reason");
    reason.setTextContent(post.flag_detail.reason);
  }

  this.container.down(".status-held").show(post.is_held);
  var has_permission = User.get_current_user_id() == post.creator_id || User.is_mod_or_higher();
  this.container.down(".activate-post").show(has_permission);
}

BrowserView.prototype.edit_show = function(shown)
{
  var post = Post.posts.get(this.displayed_post_id);
  if(!post)
    shown = false;

  if(!User.is_member_or_higher())
    shown = false;

  this.edit_shown = shown;
  this.container.down(".post-tags-box").show(!shown);
  this.container.down(".post-edit").show(shown);
  if(!shown)
  {
    /* Revert all changes. */
    this.frame_editor.discard();
    return;
  }

  this.select_edit_box(".post-edit-main");

  /* This returns [tag, tag type].  We only want the tag; we call this so we sort the
   * tags consistently. */
  var tags_by_type = Post.get_post_tags_with_type(post);
  var tags = tags_by_type.pluck(0);

  tags = tags.join(" ") + " ";

  this.container.down(".edit-tags").old_value = tags;
  this.container.down(".edit-tags").value = tags;
  this.container.down(".edit-source").value = post.source;
  this.container.down(".edit-parent").value = post.parent_id;
  this.container.down(".edit-shown-in-index").checked = post.is_shown_in_index;

  var rating_class = new Hash({ s: ".edit-safe", q: ".edit-questionable", e: ".edit-explicit" });
  this.container.down(rating_class.get(post.rating)).checked = true;

  this.edit_post_area_changed();

  this.container.down(".edit-tags").focus();
}

/* Set the size of the tag edit area to the size of its contents. */
BrowserView.prototype.edit_post_area_changed = function()
{
  var post_edit = this.container.down(".post-edit");
  var element = post_edit.down(".edit-tags");
  element.style.height = "0px";
  element.style.height = element.scrollHeight + "px";
if(0)
{
  var rating = null;
  var source = null;
  var parent_id = null;
  element.value.split(" ").each(function(tag)
  {
    /* This mimics what the server side does; it does prevent metatags from using
     * uppercase in source: metatags. */
    tag = tag.toLowerCase();
    /* rating:q or just q: */
    var m = tag.match(/^(rating:)?([qse])$/);
    if(m)
    {
      rating = m[2];
      return;
    }

    var m = tag.match(/^(parent):([0-9]+)$/);
    if(m)
    {
      if(m[1] == "parent")
        parent_id = m[2];
    }

    var m = tag.match(/^(source):(.*)$/);
    if(m)
    {
      if(m[1] == "source")
        source = m[2];
    }
  }.bind(this));

  debug("rating: " + rating);
  debug("source: " + source);
  debug("parent: " + parent_id);
}
}

BrowserView.prototype.edit_save = function()
{
  var save_completed = function()
  {
    notice("Post saved");

    /* If we're still showing the post we saved, hide the edit area. */
    if(this.displayed_post_id == post_id)
      this.edit_show(false);
  }.bind(this);
  var post_id = this.displayed_post_id;

  /* If we're in the frame editor, save it.  Don't save the hidden main editor. */
  if(this.frame_editor)
  {
    if(this.frame_editor.is_opened())
    {
      this.frame_editor.save(save_completed);
      return;
    }
  }

  var edit_tags = this.container.down(".edit-tags");
  var tags = edit_tags.value;

  /* Opera doesn't blur the field automatically, even when we hide it later. */
  edit_tags.blur();

  /* Find which rating is selected. */
  var rating_class = new Hash({ s: ".edit-safe", q: ".edit-questionable", e: ".edit-explicit" });
  var selected_rating = "s";
  rating_class.each(function(c) {
    if(this.container.down(c[1]).checked)
      selected_rating = c[0];
  }.bind(this));

  /* update_batch will give us updates for any related posts, as well as the one we're
   * updating. */
  Post.update_batch([{
    id: post_id,
    tags: this.container.down(".edit-tags").value,
    old_tags: this.container.down(".edit-tags").old_value,
    source: this.container.down(".edit-source").value,
    parent_id: this.container.down(".edit-parent").value,
    /* MI: Set value to int explicity, instead of bool, as it was converted to string,
     * then to int, ending up as 0.
     */
    is_shown_in_index: this.container.down(".edit-shown-in-index").checked ? 1 : 0,
    rating: selected_rating
  }], save_completed);
}

BrowserView.prototype.window_resize_event = function(e)
{
  if(e.stopped)
    return;
  this.update_image_window_size();
  this.scale_and_position_image(true);
}

BrowserView.prototype.toggle_view_large_image = function(from_toggle_zoom)/*Pop.*/
{
  var post = Post.posts.get(this.displayed_post_id);
  if(post == null)
    return;
  if(this.img == null)
    return;

  if(post.jpeg_url == post.sample_url)
  {
    /* There's no larger version to display. */
    return;
  }

  /* Toggle between the sample and JPEG version. */
  this.set_viewing_larger_version(!this.viewing_larger_version, from_toggle_zoom); // Pop.
  this.set_main_image(post, null, from_toggle_zoom); // XXX frame // Pop.
}

/* this.image_window_size is the size of the area where the image is visible. */
BrowserView.prototype.update_image_window_size = function()
{
  this.image_window_size = getWindowSize();

  /* If the thumb bar is shown, exclude it from the window height and fit the image
   * in the remainder.  Since the bar is at the bottom, we don't need to do anything to
   * adjust the top. */
  this.image_window_size.height -= this.thumb_bar_height;

  this.image_window_size.height = Math.max(this.image_window_size.height, 0); /* clamp to 0 if there's no space */

  /* When the window size changes, update the navigator since the cursor will resize to
   * match. */
  this.update_navigator();
}

BrowserView.prototype.scale_and_position_image = function(resizing)
{
  var img_box = this.img_box;
  if(!this.img)
    return;
  var original_width = img_box.original_width;
  var original_height = img_box.original_height;

  var post = Post.posts.get(this.displayed_post_id);
  if(!post)
  {
    debug("unexpected: displayed post " + this.displayed_post_id + " unknown");
    return;
  }

  var window_size = this.image_window_size;

  var ratio = 1.0;
  if(!this.viewing_larger_version)
  {
    /* Zoom the image to fit the viewport. */
    var ratio = window_size.width / original_width;
    if (original_height * ratio > window_size.height)
      ratio = window_size.height / original_height;
  }

  ratio *= Math.pow(0.9, this.zoom_level);

  this.displayed_image_width = Math.round(original_width * ratio);
  this.displayed_image_height = Math.round(original_height * ratio);

  this.img.width = this.displayed_image_width;
  this.img.height = this.displayed_image_height;

  this.update_canvas();

  if(this.frame_editor)
    this.frame_editor.set_image_dimensions(this.displayed_image_width, this.displayed_image_height);

  /* If we're resizing and showing the full-size image, don't snap the position
   * back to the default. */
  if(resizing && this.viewing_larger_version)
    return;

  var x = 0.5;
  var y = 0.5;
  if(this.viewing_larger_version)
  {
    /* Align the image to the top of the screen. */
    y = this.image_window_size.height/2;
    y /= this.displayed_image_height;
  }

  this.center_image_on(x, y);
}

/* x and y are [0,1]. */
BrowserView.prototype.update_navigator = function()
{
  if(!this.navigator)
    return;
  if(!this.img)
    return;

  /* The coordinates of the image located at the top-left corner of the window: */
  var scroll_x = -this.img_box.offsetLeft;
  var scroll_y = -this.img_box.offsetTop;

  /* The coordinates at the center of the window: */
  x = scroll_x + this.image_window_size.width/2;
  y = scroll_y + this.image_window_size.height/2;

  var percent_x = x / this.displayed_image_width;
  var percent_y = y / this.displayed_image_height;

  var height_percent = this.image_window_size.height / this.displayed_image_height;
  var width_percent = this.image_window_size.width / this.displayed_image_width;
  this.navigator.image_position_changed(percent_x, percent_y, height_percent, width_percent);
}

/*
 * If Canvas support is available, we can accelerate drawing.
 *
 * Most browsers are slow when resizing large images.  In the best cases, it results in
 * dragging the image around not being smooth (all browsers except Chrome).  In the worst
 * case it causes rendering the page to be very slow; in Chrome, drawing the thumbnail
 * strip under a large resized image is unusably slow.
 *
 * If Canvas support is enabled, then once the image is fully loaded, blit the image into
 * the canvas at the size we actually want to display it at.  This avoids most scaling
 * performance issues, because it's not rescaling the image constantly while dragging it
 * around.
 *
 * Note that if Chrome fixes its slow rendering of boxes *over* the image, then this may
 * be unnecessary for that browser.  Rendering the image itself is very smooth; Chrome seems
 * to prescale the image just once, which is what we're doing.
 *
 * Limitations:
 * - If full-page zooming is being used, it'll still scale at runtime.
 * - We blit the entire image at once.  It's more efficient to blit parts of the image
 *   as necessary to paint, but that's a lot more work.
 * - Canvas won't blit partially-loaded images, so we do nothing until the image is complete.
 */
BrowserView.prototype.update_canvas = function()
{
  if(!this.img.fully_loaded)
  {
    debug("image incomplete; can't render to canvas");
    return false;
  }

  if(!this.canvas)
    return;

  /* If the contents havn't changed, skip the blit.  This happens frequently when resizing
   * the window when not fitting the image to the screen. */
  if(this.canvas.rendered_url == this.img.src &&
      this.canvas.width == this.displayed_image_width &&
      this.canvas.height == this.displayed_image_height)
  {
    // debug(this.canvas.rendered_url + ", " + this.canvas.width + ", " + this.canvas.height)
    // debug("Skipping canvas blit");
    return;
  }

  this.canvas.rendered_url = this.img.src;
  this.canvas.width = this.displayed_image_width;
  this.canvas.height = this.displayed_image_height;
  var ctx = this.canvas.getContext("2d");
  ctx.drawImage(this.img, 0, 0, this.displayed_image_width, this.displayed_image_height);
  this.canvas.show();
  this.img.hide();

  return true;
}


BrowserView.prototype.center_image_on = function(percent_x, percent_y)
{
  var x = percent_x * this.displayed_image_width;
  var y = percent_y * this.displayed_image_height;

  var scroll_x = x - this.image_window_size.width/2;
  scroll_x = Math.round(scroll_x);

  var scroll_y = y - this.image_window_size.height/2;
  scroll_y = Math.round(scroll_y);

  this.img_box.setStyle({left: -scroll_x + "px", top: -scroll_y + "px"});

  this.update_navigator();
}

BrowserView.prototype.cancel_lazily_load = function()
{
  if(this.lazy_load_timer == null)
    return;

   window.clearTimeout(this.lazy_load_timer);
   this.lazy_load_timer = null;
}

/* Update the window title when the display changes. */
WindowTitleHandler = function()
{
  this.searched_tags = "";
  this.post_id = null;
  this.post_frame = null;
  this.pool = null;

  document.on("viewer:searched-tags-changed", function(e) {
    this.searched_tags = e.memo.tags || "";
    this.update();
  }.bindAsEventListener(this));

  document.on("viewer:displayed-post-changed", function(e) {
    this.post_id = e.memo.post_id;
    this.post_frame = e.memo.post_id;
    this.update();
  }.bindAsEventListener(this));

  document.on("viewer:displayed-pool-changed", function(e) {
    this.pool = e.memo.pool;
    this.update();
  }.bindAsEventListener(this));

  this.update();
}

WindowTitleHandler.prototype.update = function()
{
  var post = Post.posts.get(this.post_id);

  if(this.pool)
  {
    var title = this.pool.name.replace(/_/g, " ");

    if(post && post.pool_posts)
    {
      var pool_post = post.pool_posts.get(this.pool.id);
      if(pool_post)
      {
        var sequence = pool_post.sequence;
        title += " ";
        if(sequence.match(/^[0-9]/))
          title += "#";
        title += sequence;
      }
    }
  }
  else
  {
    var title = "/" + this.searched_tags.replace(/_/g, " ");
  }

  title += " - Browse";
  document.title = title;
}

BrowserView.prototype.parent_post_click_event = function(event)
{
  event.stop();

  var post = Post.posts.get(this.displayed_post_id);
  if(post == null || post.parent_id == null)
    return;

  this.set_post(post.parent_id);
}

BrowserView.prototype.child_posts_click_event = function(event)
{
  event.stop();

  /* Search for this post's children.  Set the results mode to center-on-current, so we
   * focus on the current item. */
  document.fire("viewer:perform-search", {
    tags: "parent:" + this.displayed_post_id,
    results_mode: "center-on-current"
  });
}

BrowserView.prototype.select_edit_box = function(className)
{
  if(this.shown_edit_container)
    this.shown_edit_container.hide();
  this.shown_edit_container = this.container.down(className);
  this.shown_edit_container.show();
}

BrowserView.prototype.show_frame_editor = function()
{
  this.select_edit_box(".frame-editor");

  /* If we're displaying a frame and not the whole image, switch to the main image. */
  var post_frame = null;
  if(this.displayed_post_frame != -1)
  {
    post_frame = this.displayed_post_frame;
    document.fire("viewer:set-active-post", {post_id: this.displayed_post_id, post_frame: -1});
  }

  this.frame_editor.open(this.displayed_post_id);
  this.container.down(".post-frames").hide();

  /* If we were on a frame when opened, focus the frame we were on.  Otherwise,
   * leave it on the default. */
  if(post_frame != null)
    this.frame_editor.focus(post_frame);
}

BrowserView.prototype.hide_frame_editor = function()
{
  this.frame_editor.discard();
  this.container.down(".post-frames").show();
}

var Navigator = function(container, target)
{
  this.container = container;
  this.target = target;
  this.hovering = false;
  this.autohide = false;
  this.img = this.container.down(".image-navigator-img");
  this.container.show();

  this.handlers = [];
  this.handlers.push(this.container.on("mousedown", this.mousedown_event.bindAsEventListener(this)));
  this.handlers.push(this.container.on("mouseover", this.mouseover_event.bindAsEventListener(this)));
  this.handlers.push(this.container.on("mouseout", this.mouseout_event.bindAsEventListener(this)));

  this.dragger = new DragElement(this.container, {
    snap_pixels: 0,
    onenddrag: this.enddrag.bind(this),
    ondrag: this.ondrag.bind(this)
  });
}

Navigator.prototype.set_image = function(image_url, width, height)
{
  this.img.src = image_url;
  this.img.width = width;
  this.img.height = height;
}

Navigator.prototype.enable = function(enabled)
{
  this.container.show(enabled);
}

Navigator.prototype.mouseover_event = function(e)
{
  if(e.relatedTarget && e.relatedTarget.isParentNode(this.container))
    return;
  debug("over " + e.target.className + ", " + this.container.className + ", " + e.target.isParentNode(this.container));
  this.hovering = true;
  this.update_visibility();
}

Navigator.prototype.mouseout_event = function(e)
{
  if(e.relatedTarget && e.relatedTarget.isParentNode(this.container))
    return;
  debug("out " + e.target.className);
  this.hovering = false;
  this.update_visibility();
}

Navigator.prototype.mousedown_event = function(e)
{
  var x = e.pointerX();
  var y = e.pointerY();
  var coords = this.get_normalized_coords(x, y);
  this.center_on_position(coords);
}

Navigator.prototype.enddrag = function(e)
{
  this.shift_lock_anchor = null;
  this.locked_to_x = null;
  this.update_visibility();
}

Navigator.prototype.ondrag = function(e)
{
  var coords = this.get_normalized_coords(e.x, e.y);
  if(e.latest_event.shiftKey != (this.shift_lock_anchor != null))
  {
    /* The shift key has been pressed or released. */
    if(e.latest_event.shiftKey)
    {
      /* The shift key was just pressed.  Remember the position we were at when it was
       * pressed. */
      this.shift_lock_anchor = [coords[0], coords[1]];
    }
    else
    {
      /* The shift key was released. */
      this.shift_lock_anchor = null;
      this.locked_to_x = null;
    }
  }

  this.center_on_position(coords);
}

Navigator.prototype.image_position_changed = function(percent_x, percent_y, height_percent, width_percent)
{
  /* When the image is moved or the visible area is resized, update the cursor rectangle. */
  var cursor = this.container.down(".navigator-cursor");
  cursor.setStyle({
    top: this.img.height * (percent_y - height_percent/2) + "px",
    left: this.img.width * (percent_x - width_percent/2) + "px",
    width: this.img.width * width_percent + "px",
    height: this.img.height * height_percent + "px"
  });
}

Navigator.prototype.get_normalized_coords = function(x, y)
{
  var offset = this.img.cumulativeOffset();
  x -= offset.left;
  y -= offset.top;
  x /= this.img.width;
  y /= this.img.height;
  return [x, y];

}

/* x and y are absolute window coordinates. */
Navigator.prototype.center_on_position = function(coords)
{
  if(this.shift_lock_anchor)
  {
    if(this.locked_to_x == null)
    {
      /* Only change the coordinate with the greater delta. */
      var change_x = Math.abs(coords[0] - this.shift_lock_anchor[0]);
      var change_y = Math.abs(coords[1] - this.shift_lock_anchor[1]);

      /* Only lock to moving vertically or horizontally after we've moved a small distance
       * from where shift was pressed. */
      if(change_x > 0.1 || change_y > 0.1)
        this.locked_to_x = change_x > change_y;
    }

    /* If we've chosen an axis to lock to, apply it. */
    if(this.locked_to_x != null)
    {
      if(this.locked_to_x)
        coords[1] = this.shift_lock_anchor[1];
      else
        coords[0] = this.shift_lock_anchor[0];
    }
  }

  coords[0] = Math.max(0, Math.min(coords[0], 1));
  coords[1] = Math.max(0, Math.min(coords[1], 1));

  this.target.fire("viewer:center-on", {x: coords[0], y: coords[1]});
}

Navigator.prototype.set_autohide = function(autohide)
{
  this.autohide = autohide;
  this.update_visibility();
}

Navigator.prototype.update_visibility = function()
{
  var box = this.container.down(".image-navigator-box");
  var visible = !this.autohide || this.hovering || this.dragger.dragging;
  box.style.visibility = visible? "visible":"hidden";
}

Navigator.prototype.destroy = function()
{
  this.dragger.destroy();

  this.handlers.each(function(h) { h.stop(); });
  this.dragger = this.handlers = null;

  this.container.hide();
}


