Post = {
  posts: new Hash(),
  tag_types: new Hash(),
  votes: new Hash(),

  tag_type_names: [
    "general",
    "artist",
    "",
    "copyright",
    "character",
    "circle",
    "faults"
  ],


	find_similar: function() {
		var old_source_name = $("post_source").name
		var old_file_name = $("post_file").name
		var old_target = $("edit-form").target
		var old_action = $("edit-form").action

		$("post_source").name = "url"
		$("post_file").name = "file"
		$("edit-form").target = "_blank"
		$("edit-form").action = "http://danbooru.iqdb.hanyuu.net/"

		$("edit-form").submit()		
		
		$("post_source").name = old_source_name
		$("post_file").name = old_file_name
		$("edit-form").target = old_target
		$("edit-form").action = old_action
	},

  make_request: function(path, params, finished)
  {
    return new Ajax.Request(path, {
      parameters: params,

      onFailure: function(req) {
        var resp = req.responseJSON;
        notice("Error: " + resp.reason);
      },

      onSuccess: function(resp) {
        var resp = resp.responseJSON
        Post.register_resp(resp);

        /* Fire posts:update, to allow observers to update their display on change. */
        var post_ids = new Hash();
        for(var i = 0; i < resp.posts.length; ++i)
          post_ids.set(resp.posts[i].id, true);

        document.fire("posts:update", {
          resp: resp,
          post_ids: post_ids
        });

        if(finished)
          finished(resp);
      }
    });
  },

  /* If delete_reason is a string, delete the post with the given reason.  If delete_reason
   * is null, approve the post.  (XXX: rename to Post.moderate) */
  approve: function(post_id, delete_reason, finished) {
    notice("Approving post #" + post_id)
    var params = {}
    params["ids[" + post_id + "]"] = "1"
    params["commit"] = delete_reason? "Delete":"Approve"
    if(delete_reason)
      params["reason"] = delete_reason

    var completion = function()
    {
      notice(delete_reason? "Post deleted":"Post approved");
      if(finished)
        finished(post_id);
      else
      {
        if ($("p" + post_id)) {
          $("p" + post_id).removeClassName("pending")
        }
        if ($("pending-notice")) {
          $("pending-notice").hide()
        }
      }
    }

    return Post.make_request("/post/moderate.json", params, completion);
  },

  undelete: function(post_id, finished)
  {
    return Post.make_request("/post/undelete.json", {id: post_id}, finished);
  },

  applied_list: [],
  reset_tag_script_applied: function() {
    for(var i=0; i < Post.applied_list.length; ++i)
      Post.applied_list[i].removeClassName("tag-script-applied");

    Post.applied_list = []
  },

  /*
   * posts is an array of the form:
   *
   * [{ id: 123, tags: "tags", old_tags: "tags2" },
   *  { id: 124, tags: "tags3", old_tags: "tags4" }]
   *
   * and we pass it as a query string that results in:
   *
   * [{ :id="123", :tags = "tags", :old_tags => "tags2" },
   *  { :id="124", :tags = "tags3", :old_tags => "tags4" }]
   *
   * Prototype won't generate a query string to do this.  We also need to hack Prototype
   * to keep it from messing around with the parameter order (bug).
   *
   * One significant difference between using this and update() is that this will
   * receive secondary updates: if you change a parent with this function, the parent
   * will have its styles (border color) updated.  update() only receives the results
   * of the post that actually changed, and won't update other affected posts.
   */
  update_batch: function(posts, finished) {
    var original_count = posts.length;

    if(TagCompletion)
    {
      /* Tell TagCompletion about recently used tags. */
      posts.each(function(post) {
        if(post.tags == null)
          return;

        TagCompletion.add_recent_tags_from_update(post.tags, post.old_tags);
      });
    }

    /* posts is a hash of id: { post }.  Convert this to a Rails-format object array. */
    var params_array = [];
    var i = 0; // Pop.
    posts.each(function(post) {
      $H(post).each(function(pair2) {
        var s = "post[" + i + "][" + pair2.key + "]=" + window.encodeURIComponent(pair2.value); // Pop.
        // var s = "post[][" + pair2.key + "]=" + window.encodeURIComponent(pair2.value);
        params_array.push(s);
      });
      i++; // Pop.
    });

    var complete = function(resp)
    {
      resp.posts.each(function(post) {
        Post.update_styles(post);

        var element = $$("#p" + post.id + " > .directlink")
        if (element.length > 0) {
          element[0].addClassName("tag-script-applied")
          Post.applied_list.push(element[0])
        }
      });

      notice((original_count == 1? "Post": "Posts") + " updated");

      if(finished)
        finished(resp.posts);
    }

    var params = params_array.join("&");
    Post.make_request("/post/update_batch.json", params, complete);
  },

  update_styles: function(post)
  {
    var e = $("p" + post.id);
    if(!e) return;
    if(post["has_children"])
      e.addClassName("has-children");
    else
      e.removeClassName("has-children");

    if(post["parent_id"])
      e.addClassName("has-parent");
    else
      e.removeClassName("has-parent");
  },

  /* Deprecated; use Post.update_batch instead. */
  update: function(post_id, params, finished) {
    notice('Updating post #' + post_id)
    params["id"] = post_id

    new Ajax.Request('/post/update.json', {
      parameters: params,

      onComplete: function(resp) {
        var resp = resp.responseJSON

        if (resp.success) {
          notice('Post updated')

          // Update the stored post.
          Post.register(resp.post)
          Post.register_tags(resp.tags);

          Post.update_styles(resp.post);

          var element = element = $$("#p" + post_id + " > .directlink")
          if (element.length > 0) {
            element[0].addClassName("tag-script-applied")
            Post.applied_list.push(element[0])
          }

          if(finished)
            finished(resp.post);
        } else {
          notice('Error: ' + resp.reason)
        }
      }
    })
  },

  activate_posts: function(post_ids, finished)
  {
    notice("Activating " + post_ids.length + (post_ids.length == 1? " post":" posts"));
    var params = {};
    params["post_ids[]"] = post_ids

    new Ajax.Request('/post/activate.json', {
      parameters: params,

      onComplete: function(resp) {
        var resp = resp.responseJSON

        if (resp.success) {
          if(finished)
            finished(resp);
        } else {
          notice('Error: ' + resp.reason)
        }
      }
    })
  },

  activate_all_posts: function()
  {
    var post_ids = [];
    Post.posts.each(function(pair) {
      /* Only activate posts that are actually displayed; we may have others registered. */
      if($("p" + pair.key))
        post_ids.push(pair.key);
    });
    Post.activate_posts(post_ids, function(resp) {
      if(resp.count == 0)
        notice("No posts were activated.");
      else
        notice(resp.count + (resp.count == 1? " post":" posts") + " activated");
    });
  },

  /* Activating a single post uses post/update, which returns the finished post, so we can
   * check if the change was made.  activate_posts uses post/activate, which works in bulk
   * and doesn't return errors for individual posts. */
  activate_post: function(post_id)
  {
     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
         $("held-notice").remove();
     });
  },


  init_add_to_favs: function(post_id, add_to_favs, remove_from_favs) {
    var update_add_to_favs = function(e)
    {
      if(e != null && e.memo.post_ids.get(post_id) == null)
        return;
      var vote = Post.votes.get(post_id) || 0;
      add_to_favs.show(vote < 3);
      remove_from_favs.show(vote >= 3);
    }

    update_add_to_favs();
    document.on("posts:update", update_add_to_favs);
  },

  vote: function(post_id, score) {
    if(score > 3)
      return;

    notice("Voting...")
    Post.make_request("/post/vote.json", { id: post_id, score: score }, function(resp) { notice("Vote saved"); });
  },

  flag: function(id, finished) {
    var reason = prompt("Why should this post be flagged for deletion?", "")
    if (!reason)
      return false;

    var complete = function()
    {
      notice("Post was flagged for deletion");
      if(finished)
        finished(id);
      else
      {
        var e = $("p" + id);
        if(e)
          e.addClassName("flagged");
      }
    }

    return Post.make_request("/post/flag.json", { "id": id, "reason": reason }, complete);
  },

  unflag: function(id, finished) {
    var complete = function()
    {
      notice("Post was approved");
      if(finished)
        finished(id);
      else
      {
        var e = $("p" + id);
        if(e)
          e.removeClassName("flagged");
      }
    }

    return Post.make_request("/post/flag.json", { id: id, unflag: 1 }, complete);
  },

  observe_text_area: function(field_id) {
    $(field_id).observe("keydown", function(e) {
      if (e.keyCode == Event.KEY_RETURN) {
        e.stop();
        this.up("form").simulate_submit();
      }
    })
  },

  /*
   * Group tags by type.
   *
   * Post.get_post_tags_by_type(post)
   * -> {general: ["tagme"], faults: ["fixme", "crease"]}
   */
  get_post_tags_by_type: function(post)
  {
    var results = new Hash;

    post.tags.each(function(tag)
    {
      var tag_type = Post.tag_types.get(tag);

      /* We can end up not knowing a tag's type due to tag script editing giving us
       * tags we weren't told the type of. */
      if(!tag_type)
        tag_type = "general";
      var list = results.get(tag_type);
      if(!list)
      {
        list = [];
        results.set(tag_type, list);
      }
      list.push(tag);
    });

    return results;
  },

  /*
   * Get post tags with their types.
   *
   * Post.get_post_tags_with_type(post)
   * -> [["tagme", "general"], ["fixme", "faults"], ["crease", "faults"]]
   *
   * The results will be sorted by type.
   */
  get_post_tags_with_type: function(post)
  {
    var tag_types = Post.get_post_tags_by_type(post);
    var types = tag_types.keys();

    var type_order = ["artist", "circle", "copyright", "character", "faults", "general"];
    types = types.sort(function(a, b) {
      var a_idx = type_order.indexOf(a);
      if(a_idx == -1) a_idx = 999;
      var b_idx = type_order.indexOf(b);
      if(b_idx == -1) b_idx = 999;
      return a_idx - b_idx;
    });

    var results = new Array;
    types.each(function(type) {
      var tags = tag_types.get(type);
      tags.each(function(tag) {
        results.push([tag, type]);
      });
    });
    return results;
  },

  /* Register all data from a generic post response. */
  register_resp: function(resp) {
    if(resp.posts)
      Post.register_posts(resp.posts);
    if(resp.tags)
      Post.register_tags(resp.tags);
    if(resp.votes)
      Post.register_votes(resp.votes);
    if(resp.pools)
      Pool.register_pools(resp.pools);
    if(resp.pool_posts)
      Pool.register_pool_posts(resp.pool_posts, resp.posts);
  },

  register: function(post) {
    post.tags = post.tags.match(/\S+/g) || []
    post.match_tags = post.tags.clone()
    post.match_tags.push("rating:" + post.rating.charAt(0))
    post.match_tags.push("status:" + post.status)

    this.posts.set(post.id, post)
  },

  register_posts: function(posts) {
    posts.each(function(post) { Post.register(post); });
  },

  unregister_all: function() {
    this.posts = new Hash();
  },

  /* Post.register_tags({tagme: "general"}); */
  register_tags: function(tags, no_send_to_completion) {
    this.tag_types.update(tags);

    /* If no_send_to_completion is true, this data is coming from completion, so there's
     * no need to send it back. */
    if(TagCompletion && !no_send_to_completion)
      TagCompletion.update_tag_types();
  },

  /* Post.register_votes({12345: 1}) */
  register_votes: function(votes) {
    this.votes.update(votes);
  },

  blacklists: [],

  is_blacklisted: function(post_id) {
    var post = Post.posts.get(post_id)
    var has_tag = function(tag) { return post.match_tags.indexOf(tag) != -1; };

    /* This is done manually, since this needs to be fast and Prototype's functions are
     * too slow. */
    var blacklist_applies = function(b)
    {
      var require = b.require;
      var require_len = require.length;
      for(var j = 0; j < require_len; ++j)
      {
        if(!has_tag(require[j]))
          return false;
      }

      var exclude = b.exclude;
      var exclude_len = exclude.length;
      for(var j = 0; j < exclude_len; ++j)
      {
        if(has_tag(exclude[j]))
          return false;
      }

      return true;
    }

    var blacklists = Post.blacklists;
    var len = blacklists.length;
    for(var i = 0; i < len; ++i)
    {
      var b = blacklists[i];
      if(blacklist_applies(b))
        return true;
    }
    return false;
  },

  apply_blacklists: function() {	
    Post.blacklists.each(function(b) { b.hits = 0 })

    var count = 0
    Post.posts.each(function(pair) {
      var thumb = $("p" + pair.key)
      if (!thumb) return

      var post = pair.value

      var has_tag = post.match_tags.member.bind(post.match_tags)
      post.blacklisted = []
      if(post.id != Post.blacklist_options.exclude)
      {
        Post.blacklists.each(function(b) {
          if (b.require.all(has_tag) && !b.exclude.any(has_tag)) {
            b.hits++
            if (!Post.disabled_blacklists[b.tags]) post.blacklisted.push(b)
          }
        })
      }
      var bld = post.blacklisted.length > 0

      /* The class .javascript-hide hides elements only if JavaScript is enabled, and is
       * applied to all posts by default; we remove the class to show posts.  This prevents
       * posts from being shown briefly during page load before this script is executed,
       * but also doesn't break the page if JavaScript is disabled. */
      count += bld
      if (Post.blacklist_options.replace)
      {
        if(bld)
        {
          thumb.src = "/images/blank.gif";

          /* Trying to work around Firefox displaying the old thumb.src briefly before loading
           * the blacklisted thumbnail, even though they're applied at the same time: */
          var f = function(event)
          {
            var img = event.target;
            img.stopObserving("load");
            img.stopObserving("error");
            img.src = "/blacklisted-preview.png";
            img.removeClassName("javascript-hide");
          }
          thumb.observe("load", f)
          thumb.observe("error", f)
        }
        else
        {
          thumb.src = post.preview_url;
          thumb.removeClassName("javascript-hide");
        }
      }
      else
      {
        if(bld)
          thumb.addClassName("javascript-hide").addClassName('blacklisted-post'); // Pop.
        else
          thumb.removeClassName("javascript-hide");
      }
    })

    if (Post.countText)
      Post.countText.update(count);

    var notice = $("blacklisted-notice");
    if(notice)
      notice.show(count > 0);

    return count
  },

  // When blacklists are added dynamically and saved, add them here so we don't have to try
  // to edit the cookie in-place.
  current_blacklists: null,
  hide_inactive_blacklists: true,
  disabled_blacklists: {},

  blacklists_update_disabled: function() {
    Post.blacklists.each(function(b) {
      if(!b.a)
        return;
      if(Post.disabled_blacklists[b.tags] || b.hits == 0)
        b.a.addClassName("blacklisted-tags-disabled");
      else
        b.a.removeClassName("blacklisted-tags-disabled");
    });
  },

  // XXX: we lose exclude and replace when we're re-called
  init_blacklisted: function(options) {
    Post.blacklist_options = Object.extend({
      replace: false,
      exclude: null
    }, options);
    var bl_entries;
    if(Post.current_blacklists)
      bl_entries = Post.current_blacklists;
    else
    {
      // bl_entries = jQuery.parseJSON(jQuery.cookie('blacklisted_tags')) || [];
      bl_entries = eval(jQuery.cookie("blacklisted_tags")); // Pop.
    }

    Post.blacklists = [];
    bl_entries.each(function(val) {
        var s = val.replace(/(rating:[qes])\w+/, "$1")
        var tags = s.match(/\S+/g)
        if (!tags) return
        var b = { tags: tags, original_tag_string: val, require: [], exclude: [], hits: 0 }
        tags.each(function(tag) {
          if (tag.charAt(0) == '-') b.exclude.push(tag.slice(1))
          else b.require.push(tag)
        })
        Post.blacklists.push(b)
    })

    Post.countText = $("blacklist-count")
    if(Post.countText)
      Post.countText.update("");

    Post.apply_blacklists();

    var sidebar = $("blacklisted-sidebar")
    if (sidebar)
      sidebar.show()

    var list = $("blacklisted-list")
    if(list)
    {
      while(list.firstChild)
        list.removeChild(list.firstChild);

      Post.blacklists.sort(function(a, b) {
        if(a.hits == 0 && b.hits > 0) return 1;
        if(a.hits > 0 && b.hits == 0) return -1;
        return a.tags.join(" ").localeCompare(b.tags.join(" "));
      });

      inactive_blacklists_hidden = 0
      Post.blacklists.each(function(b) {
        if (Post.hide_inactive_blacklists && !b.hits)
        {
          ++inactive_blacklists_hidden;
          return;
        }


        var li = list.appendChild(document.createElement("li"))
        li.className = "blacklisted-tags"
        li.style.position = "relative";

        var del = li.appendChild($(document.createElement("a")));
        del.style.position = "absolute";
        del.style.left = "-0.75em";
        del.href = "#";
        del.update("⊘");

        del.observe("click", function(event) {
          /* We need to call run_login_onclick ourself, since this form isn't created with the form helpers. */
          if(!User.run_login_onclick(event)) return false;

          event.stop();

          var tag = b.original_tag_string;
          User.modify_blacklist([], [tag], function(resp) {
            notice("Unblacklisted \"" + tag + "\"");

            Post.current_blacklists = resp.result;
            Post.init_blacklisted();
          });
        });

        li.appendChild(document.createTextNode("» "));

        var a = li.appendChild(document.createElement("a"))
        b.a = a;
        a.href = "#"
        a.className = "no-focus-outline"

        if(!b.hits) {
          a.addClassName("blacklisted-tags-disabled");
        } else {
          $(a).observe("click", function(event) {
            Post.disabled_blacklists[b.tags] = !Post.disabled_blacklists[b.tags]

            Post.apply_blacklists()
            Post.blacklists_update_disabled();
            event.stop()
          });
        }

        var tags = a.appendChild(document.createTextNode(b.tags.join(" ")));
        li.appendChild(document.createTextNode(" "));
        var span = li.appendChild(document.createElement("span"))
        span.className = "post-count"
        if(b.hits > 0)
          span.appendChild(document.createTextNode("(" + b.hits + ")"));
      })

      /* Add the "Show all blacklists" button.  If Post.hide_inactive_blacklists is false, then
       * we've already clicked it and hidden it, so don't recreate it. */
      if(Post.hide_inactive_blacklists && inactive_blacklists_hidden > 0)
      {
        var li = list.appendChild(document.createElement("li"))
        li.className = "no-focus-outline"
        li.id = "blacklisted-tag-show-all"

        var a = li.appendChild(document.createElement("a"))
        a.href = "#"
        a.className = "no-focus-outline"

        $(a).observe("click", function(event) {
          event.stop();
          $("blacklisted-tag-show-all").hide();
          Post.hide_inactive_blacklists = false;
          Post.init_blacklisted();
        });

        var tags = a.appendChild(document.createTextNode("» Show all blacklists"));
        li.appendChild(document.createTextNode(" "));
      }
    }

    Post.blacklists_update_disabled();
  },

  blacklist_add_commit: function()
  {
    var tag = $("add-blacklist").value;
    if(tag == "")
      return;

    $("add-blacklist").value = "";
    User.modify_blacklist(tag, [], function(resp) {
      notice("Blacklisted \"" + tag + "\"");

      Post.current_blacklists = resp.result;
      Post.init_blacklisted();
    });
  },

  last_click_id: null,
  check_avatar_blacklist: function(post_id, id)
  {
    if(id && id == this.last_click_id)
      return true;
    this.last_click_id = id;

    if(!Post.is_blacklisted(post_id))
      return true;

    notice("This post matches one of your blacklists.  Click again to open.");
    return false;
  },

  resize_image: function() {
    var img = $("image");

    if(img.original_width == null)
    {
      img.original_width = img.width;
      img.original_height = img.height;
    }

    var ratio = 1;
    if ((img.scale_factor == 1) || (img.scale_factor == null)) {
      /* Use clientWidth for sizing the width, and the window height for the height.
       * This prevents needing to scroll horizontally to center the image. */
      var client_width = $("right-col").clientWidth - 15;
      var client_height = window.innerHeight - 15;
      ratio = Math.min(ratio, client_width / img.original_width);
      ratio = Math.min(ratio, client_height / img.original_height);
    }
    img.width = img.original_width * ratio;
    img.height = img.original_height * ratio;
    img.scale_factor = ratio;

    if (window.Note) {
      for (var i=0; i<window.Note.all.length; ++i) {
        window.Note.all[i].adjustScale()
      }
    }
  },

  get_scroll_offset_to_center: function(element)
  {
    var window_size = document.viewport.getDimensions();
    var offset = element.cumulativeOffset();
    var left_spacing = (window_size.width - element.offsetWidth) / 2;
    var top_spacing = (window_size.height - element.offsetHeight) / 2;
    var scroll_x = offset.left - left_spacing;
    var scroll_y = offset.top - top_spacing;
    return [scroll_x, scroll_y];
  },
  center_image: function(img)
  {
    /* Make sure we have enough space to scroll far enough to center the image.  Set a
     * minimum size on the body to give us more space on the right and bottom, and add
     * a padding to the image to give more space on the top and left. */
    if(!img)
      img = $("image");
    if(!img)
      return;

    /* Any existing padding (possibly from a previous call to this function) will be
     * included in cumulativeOffset and throw things off, so clear it. */
    img.setStyle({paddingLeft: 0, paddingTop: 0});

    var target_offset = Post.get_scroll_offset_to_center(img);
    var padding_left = -target_offset[0];
    if(padding_left < 0) padding_left = 0;
    img.setStyle({paddingLeft: padding_left + "px"});

    var padding_top = -target_offset[1];
    if(padding_top < 0) padding_top = 0;
    img.setStyle({paddingTop: padding_top + "px"});

    var window_size = document.viewport.getDimensions();
    var required_width = target_offset[0] + window_size.width;
    var required_height = target_offset[1] + window_size.height;
    $(document.body).setStyle({minWidth: required_width + "px", minHeight: required_height + "px"});

    /* Resizing the body may shift the image to the right, since it's centered in the content.
     * Recalculate offsets with the new cumulativeOffset. */
    var target_offset = Post.get_scroll_offset_to_center(img);
    window.scroll(target_offset[0], target_offset[1]);
  },

  scale_and_fit_image: function(img)
  {
    if(!img)
      img = $("image");
    if(!img)
      return;

    if(img.original_width == null)
    {
      img.original_width = img.width;
      img.original_height = img.height;
    }
    var window_size = document.viewport.getDimensions();
    var client_width = window_size.width;
    var client_height = window_size.height;

    /* Zoom the image to fit the viewport. */
    var ratio = client_width / img.original_width;
    if (img.original_height * ratio > client_height)
      ratio = client_height / img.original_height;
    if(ratio < 1)
    {
      img.width = img.original_width * ratio;
      img.height = img.original_height * ratio;
    }

    this.center_image(img);

    Post.adjust_notes();
  },

  adjust_notes: function() {
    if (!window.Note)
      return;
    for (var i=0; i<window.Note.all.length; ++i) {
      window.Note.all[i].adjustScale()
    }
  },


  highres: function() {
    var img = $("image");
    if(img.already_resized)
      return;
    img.already_resized = true;

    // un-resize
    if ((img.scale_factor != null) && (img.scale_factor != 1)) {
      Post.resize_image();
    }

    var f = function() {
      img.original_height = null;
      img.original_width = null;
      var highres = $("highres-show");
      img.height = highres.getAttribute("link_height");
      img.width = highres.getAttribute("link_width");
      $("note-container").insert({after: img});
      img.src = highres.href;

      if (window.Note) {
        window.Note.all.invoke("adjustScale")
      }
    }

    // Clear the image before loading the new one, so it doesn't show the old image
    // at the new resolution while the new one loads.  Hide it, so we don't flicker
    // a placeholder frame.
    if($('resized_notice'))
      $('resized_notice').hide();
    img.height = img.width = 0;
    img.src = "";
    img.remove();
    f(img);
  },

  set_same_user: function(creator_id)
  {
    var old = $("creator-id-css");
    if(old)
      old.parentNode.removeChild(old);

    var css = ".creator-id-"+ creator_id + " .directlink { background-color: #300 !important; }";
    var style = document.createElement("style");
    style.id = "creator-id-css";
    style.type = "text/css";
    if(style.styleSheet) // IE
      style.styleSheet.cssText = css;
    else
      style.appendChild(document.createTextNode(css));
    document.getElementsByTagName("head")[0].appendChild(style);
  },

  init_post_list: function()
  {
    Post.posts.each(function(p)
    {
      var post_id = p[0]
      var post = p[1]
      var directlink = $("p" + post_id)
      if (!directlink)
        return;
      directlink = directlink.down(".directlink")
      if (!directlink)
        return;
      directlink.observe('mouseover', function(event) { Post.set_same_user(post.creator_id); return false; }, true);
      directlink.observe('mouseout', function(event) { Post.set_same_user(null); return false; }, true);
    });
  },

  init_hover_thumb: function(hover, post_id, thumb, container)
  {
    /* Hover thumbs trigger rendering bugs in IE7. */
    if(Prototype.Browser.IE)
      return;
    hover.observe("mouseover", function(e) { Post.hover_thumb_mouse_over(post_id, hover, thumb, container); });
    hover.observe("mouseout", function(e) { if(e.relatedTarget == thumb) return; Post.hover_thumb_mouse_out(thumb); });
    if(!thumb.hover_init) {
      thumb.hover_init = true;
      thumb.observe("mouseout", function(e) { Post.hover_thumb_mouse_out(thumb); });
    }

  },

  hover_thumb_mouse_over: function(post_id, AlignItem, image, container)
  {
    var post = Post.posts.get(post_id);
    image.hide();

    var offset = AlignItem.cumulativeOffset();
    image.style.width = "auto";
    image.style.height = "auto";
    if(Post.is_blacklisted(post_id))
    {
      image.src = "/images/blacklisted-preview.png";
    }
    else
    {
      image.src = post.preview_url;
      if(post.status != "deleted")
      {
        image.style.width = post.actual_preview_width + "px";
        image.style.height = post.actual_preview_height + "px";
      }
    }

    var container_top = container.cumulativeOffset().top;
    var container_bottom = container_top + container.getHeight() - 1;

    /* Normally, align to the item we're hovering over.  If the image overflows over
     * the bottom edge of the container, shift it upwards to stay in the container,
     * unless the container's too small and that would put it over the top. */
    var y = offset.top-2; /* -2 for top 2px border */
    if(y + image.getHeight() > container_bottom)
    {
      var bottom_aligned_y = container_bottom - image.getHeight() - 4; /* 4 for top 2px and bottom 2px borders */
      if(bottom_aligned_y >= container_top)
        y = bottom_aligned_y;
    }

    image.style.top = y + "px";
    image.show();
  },

  hover_thumb_mouse_out: function(image)
  {
    image.hide();
  },

  acknowledge_new_deleted_posts: function(post_id) {
    new Ajax.Request("/post/acknowledge_new_deleted_posts.json", {
      onComplete: function(resp) {
        var resp = resp.responseJSON

        if (resp.success)
	{
          if ($("posts-deleted-notice"))
            $("posts-deleted-notice").hide()
        } else {
          notice("Error: " + resp.reason)
        }
      }
    })
  },

  hover_info_pin: function(post_id)
  {
    var post = null;
    if(post_id != null)
      post = Post.posts.get(post_id);
    Post.hover_info_pinned_post = post;
    Post.hover_info_update();
  },

  hover_info_mouseover: function(post_id)
  {
    var post = Post.posts.get(post_id);
    if(Post.hover_info_hovered_post == post)
      return;
    Post.hover_info_hovered_post = post;
    Post.hover_info_update();
  },

  hover_info_mouseout: function()
  {
    if(Post.hover_info_hovered_post == null)
      return;
    Post.hover_info_hovered_post = null;
    Post.hover_info_update();
  },

  hover_info_hovered_post: null,
  hover_info_displayed_post: null,
  hover_info_shift_held: false,
  hover_info_pinned_post: null, /* pinned by something like the edit menu; shift state and mouseover is ignored */

  hover_info_update: function()
  {
    var post = Post.hover_info_pinned_post;
    if(!post)
    {
      post = Post.hover_info_hovered_post;
      if(!Post.hover_info_shift_held)
        post = null;
    }

    if(Post.hover_info_displayed_post == post)
      return;
    Post.hover_info_displayed_post = post;

    var hover = $("index-hover-info");
    var overlay = $("index-hover-overlay");
    if(!post)
    {
      hover.hide();
      overlay.hide();
      overlay.down("IMG").src = "/images/blank.gif";
      return;
    }
    hover.down("#hover-dimensions").innerHTML = post.width + "x" + post.height;
    hover.select("#hover-tags SPAN A").each(function(elem) {
      elem.innerHTML = "";
    });
    var tags_by_type = Post.get_post_tags_by_type(post);
    tags_by_type.each(function(key) {
      var elem = $("hover-tag-" + key[0]);
      var list = []
      key[1].each(function(tag) { list.push(tag); });
      elem.innerHTML = list.join(" ");
    });
    if(post.rating=="s")
      hover.down("#hover-rating").innerHTML = "s";
    else if(post.rating=="q")
      hover.down("#hover-rating").innerHTML = "q";
    else if(post.rating=="e")
      hover.down("#hover-rating").innerHTML = "e";
    hover.down("#hover-post-id").innerHTML = post.id;
    hover.down("#hover-score").innerHTML = post.score;
    if(post.is_shown_in_index)
      hover.down("#hover-not-shown").hide();
    else
      hover.down("#hover-not-shown").show();
    hover.down("#hover-is-parent").show(post.has_children);
    hover.down("#hover-is-child").show(post.parent_id != null);
    hover.down("#hover-is-pending").show(post.status == "pending");
    hover.down("#hover-is-flagged").show(post.status == "flagged");
    var set_text_content = function(element, text)
    {
      (element.innerText || element).textContent = text;
    }

    if(post.status == "flagged")
    {
      hover.down("#hover-flagged-reason").setTextContent(post.flag_detail.reason);
      hover.down("#hover-flagged-by").setTextContent(post.flag_detail.flagged_by);
    }

    hover.down("#hover-file-size").innerHTML = number_to_human_size(post.file_size);
    hover.down("#hover-author").innerHTML = post.author;
    hover.show();

    /* Reset the box to 0x0 before polling the size, so it expands to its maximum size,
     * and read the size. */
    hover.style.left = "0px";
    hover.style.top = "0px";
    var hover_width = hover.scrollWidth;
    var hover_height = hover.scrollHeight;

    var hover_thumb = $("p" + post.id).down("IMG");
    var thumb_offset = hover_thumb.cumulativeOffset();
    var thumb_center_x = thumb_offset[0] + hover_thumb.scrollWidth/2;
    var thumb_top_y = thumb_offset[1];
    var x = thumb_center_x - hover_width/2;
    var y = thumb_top_y - hover_height;

    /* Clamp the X coordinate so the box doesn't fall off the side of the screen.  Don't
     * clamp Y. */
    var client_width = document.viewport.getDimensions()["width"];
    if(x < 0) x = 0;
    if(x + hover_width > client_width) x = client_width - hover_width;
    hover.style.left = x + "px";
    hover.style.top = y + "px";

    overlay.down("A").href = (User.get_use_browser()?  "/post/browse#":"/post/show/") + post.id;
    overlay.down("IMG").src = post.preview_url;

    /* This doesn't always align properly in Firefox if full-page zooming is being
     * used. */
    var x = thumb_center_x - post.actual_preview_width/2;
    var y = thumb_offset[1];
    overlay.style.left = x + "px";
    overlay.style.top = y + "px";
    overlay.show();
  },

  hover_info_shift_down: function()
  {
    if(Post.hover_info_shift_held)
      return;
    Post.hover_info_shift_held = true;
    Post.hover_info_update();
  },

  hover_info_shift_up: function()
  {
    if(!Post.hover_info_shift_held)
      return;
    Post.hover_info_shift_held = false;
    Post.hover_info_update();
  },

  hover_info_init: function()
  {
    document.observe("keydown", function(e) {
      if(e.keyCode != 16) /* shift */
        return;
      Post.hover_info_shift_down();
    });

    document.observe("keyup", function(e) {
      if(e.keyCode != 16) /* shift */
        return;
      Post.hover_info_shift_up();
    });

    document.observe("blur", function(e) { Post.hover_info_shift_up(); });

    var overlay = $("index-hover-overlay");
    Post.posts.each(function(p) {
      var post_id = p[0]
      var post = p[1]

      var span = $("p" + post.id);
      if(span == null)
        return;

      span.down("A").observe("mouseover", function(e) { Post.hover_info_mouseover(post_id); });
      span.down("A").observe("mouseout", function(e) { if(e.relatedTarget && e.relatedTarget.isParentNode(overlay)) return; Post.hover_info_mouseout(); });
    });

    overlay.observe("mouseout", function(e) { Post.hover_info_mouseout(); });
  },

  highlight_posts_with_tag: function(tag)
  {
    Post.posts.each(function(p) {
      var post_id = p[0]
      var post = p[1]
      var thumb = $("p" + post.id);
      
      if(!thumb)
        return;
// console.log(tag, post.tags);
      if(tag && post.tags.indexOf(tag) != -1)
      {
        thumb.addClassName("highlighted-post");
      } else {
        thumb.removeClassName("highlighted-post");
      }
    });
  },

  reparent_post: function(post_id, old_parent_id, has_grandparent, finished)
  {
    /* If the parent has a parent, this is too complicated to handle automatically. */
    if(has_grandparent)
    {
      alert("The parent post has a parent, so this post can't be automatically reparented.");
      return;
    }

    /*
     * Request a list of child posts.
     * The parent post itself will be returned by parent:.  This is expected; it'll cause us
     * to parent the post to itself, which unparents it from the old parent.
     */
    var change_requests = [];
    new Ajax.Request("/post.json", {
      parameters: { tags: "parent:" + old_parent_id },

      onComplete: function(resp) {
        var resp = resp.responseJSON
	for(var i = 0; i < resp.length; ++i)
	{
          var post = resp[i];
          if(post.id == old_parent_id && post.parent_id != null)
          {
            alert("The parent post has a parent, so this post can't be automatically reparented.");
            return;
          }
	  change_requests.push({ id: resp[i].id, tags: "parent:" + post_id, old_tags: "" });
        }

	/* We have the list of changes to make in change_requests.  Send a batch
	 * request. */
        if(finished == null)
          finished = function() { document.location.reload() };
	Post.update_batch(change_requests, finished);
      }
    });
  },
  get_url_for_post_in_pool: function(post_id, pool_id)
  {
    return "/post/show/" + post_id + "?pool_id=" + pool_id;
  },
  jump_to_post_in_pool: function(post_id, pool_id)
  {
    if(post_id == null)
    {
      notice("No more posts in this pool");
      return;
    }
    window.location.href = Post.get_url_for_post_in_pool(post_id, pool_id);
  },

  /*
   * If the user has global browser links enabled, apply them.  This changes all links
   * from /post/show/123 to /post/browse#123, and /pool/show/123 to /post/browse#/pool:123.
   *
   * We do this in JS, so it applies without affecting memcached pages, and applies to
   * things like preformatted, translated DText blocks.
   *
   * This is only done if the user's "Use post browser" (User.use_browser) setting is enabled.
   */
  InitBrowserLinks: function()
  {
    if(!User.get_use_browser())
      return;

    /*
     * Break out the controller, action, ID and anchor:
     * http://url.com/post/show/123#anchor
     */
    var parse_url = function(href)
    {
      var match = href.match(/^(https?:\/\/[^\/]+)\/([a-z]+)\/([a-z]+)\/([0-9]+)([^#]*)(#.*)?$/);
      if(!match)
        return null;

      return {
        controller: match[2],
        action: match[3],
        id: match[4],
        hash: match[6]
      };
    }

    /*
     * Parse an index search URL and return the tags.  Only accept URLs with no other parameters;
     * this shouldn't match the paginator in post/index.
     *
     * http://url.com/post?tags=tagme
     */
    var parse_index_url = function(href)
    {
      var match = href.match(/^(https?:\/\/[^\/]+)\/post(\/index)?\?tags=([^&]*)$/);
      if(!match)
        return null;
      return match[3];
    }

    /* If the current page is /pool/show, make post links include both the post ID and
     * the pool ID, eg. "#12345/pool:123". */
    var current = parse_url(document.location.href);
    var current_pool_id = null;
    if(current && current.controller == "pool" && current.action == "show")
      current_pool_id = current.id;

    $$("A").each(function(a) {
      if(a.hasClassName("no-browser-link") || a.up(".no-browser-link"))
        return;

      var tags = parse_index_url(a.href);
      if(tags != null)
      {
        a.href = "/post/browse#/" + tags;
        return;
      }
      var target = parse_url(a.href);
      if(!target)
        return;

      /* If the URL has a hash, then it's something like a post comment link, so leave it
       * alone. */
      if(target.hash)
        return;

      if(target.controller == "post" && target.action == "show")
      {
        var url = "/post/browse#" + target.id;
        if(current_pool_id != null)
          url += "/pool:" + current_pool_id;
        a.browse_href = url;
        a.orig_href = a.href;
      }
      else if(target.controller == "pool" && target.action == "show")
      {
        a.browse_href = "/post/browse#/pool:" + target.id;
        a.orig_href = a.href;
      }

      if(a.browse_href)
        a.href = a.browse_href;
    });
  },

  /* Handle the sample URL cache.  This allows pages that statically know sample URLs for
   * files (post/index) to communicate that to dynamic pages that normally get it from
   * XHR (post/browse). */
  cached_sample_urls: null,

  /* Return an object containing cached sample URLs, eg. {"12345": "http://example.com/image.jpg"}.
   * If the browser lacks support for this, return null.  If the stored data is invalid or doesn't
   * yet exist, return {}. */
  get_cached_sample_urls: function()
  {
    if(LocalStorageDisabled())
      return null;

    /* If the data format is out of date, clear it. */
    if(localStorage.sample_url_format != 2)
      Post.clear_sample_url_cache();

    if(Post.cached_sample_urls != null)
      return Post.cached_sample_urls;

    try {
      var sample_urls = JSON.parse(window.localStorage.sample_urls);
    } catch(SyntaxError) {
      return {};
    }

    if(sample_urls == null)
      return {};

    Post.cached_sample_urls = sample_urls;
    return sample_urls;
  },

  clear_sample_url_cache: function()
  {
    if("sample_urls" in localStorage)
      delete window.localStorage.sample_urls;
    if("sample_url_fifo" in localStorage)
      delete window.localStorage.sample_url_fifo;
    localStorage.sample_url_format = 2;
  },

  /* Save all loaded posts to the sample URL cache, and expire old data. */
  cache_sample_urls: function()
  {
    var sample_urls = Post.get_cached_sample_urls();
    if(sample_urls == null)
      return;

    /* Track post URLs in the order we see them, and push old data out. */
    var fifo = window.localStorage.sample_url_fifo || null;
    fifo = fifo? fifo.split(","): [];

    Post.posts.each(function(id_and_post) {
      var post = id_and_post[1];
      if(post.sample_url)
        sample_urls[post.id] = post.sample_url;
      fifo.push(post.id);
    });

    /* Erase all but the most recent 1000 items. */
    fifo = fifo.splice(-1000);

    /* Make a set of the FIFO, so we can do lookups quickly. */
    var fifo_set = {}
    fifo.each(function(post_id) { fifo_set[post_id] = true; });

    /* Make a list of items no longer in the FIFO to be deleted. */
    var post_ids_to_expire = [];
    for(post_id in sample_urls)
    {
      if(!(post_id in fifo_set))
        post_ids_to_expire.push(post_id);
    }

    /* Erase items no longer in the FIFO. */
    post_ids_to_expire.each(function(post_id) { delete sample_urls[post_id]; });

    /* Save the cached items and FIFO back to localStorage. */
    Post.cached_sample_urls = sample_urls;
    try {
      window.localStorage.sample_urls = JSON.stringify(sample_urls);
      window.localStorage.sample_url_fifo = fifo.join(",");
    } catch(e) {
      /* If this fails for some reason, clear the data. */
      Post.clear_sample_url_cache();
      throw(e);
    }
  },

  prompt_to_delete: function(post_id, completed)
  {
    if(completed == null)
      completed = function() { window.location.reload(); };

    var flag_detail = Post.posts.get(post_id).flag_detail
    var default_reason = flag_detail? flag_detail.reason:"";
    var reason = prompt('Reason:', default_reason);
    if(!reason)
      return false;

    Post.approve(post_id, reason, completed);
    return true;
  }
}
