var DANBOORU_VERSION = {
  major: 1,
  minor: 13,
  build: 0
}

/* If initial is true, this is a notice set by the notice cookie and not a
 * realtime notice from user interaction. */
function notice(msg, initial) {
  /* If this is an initial notice, and this screen has a dedicated notice
   * container other than the floating notice, use that and don't disappear
   * it. */
  if(initial) {
    var static_notice = $("static_notice");
    if(static_notice) {
      static_notice.update(msg);
      static_notice.show();
      return;
    }
  }

  start_notice_timer();
  $('notice').update(msg);
  $('notice-container').show();
}

function number_to_human_size(size, precision)
{
  if(precision == null)
    precision = 1;

  size = Number(size);
  if(size.toFixed(0) == 1) text = "1 Byte";
  else if(size < 1024)                  text = size.toFixed(0) + " Bytes";
  else if(size < 1024*1024)             text = (size / 1024).toFixed(precision) + " KB";
  else if(size < 1024*1024*1024)        text = (size / (1024*1024)).toFixed(precision) + " MB";
  else if(size < 1024*1024*1024*1024)   text = (size / (1024*1024*1024)).toFixed(precision) + " GB";
  else                                  text = (size / (1024*1024*1024*1024)).toFixed(precision) + " TB";

  text = text.gsub(/([0-9]\.\d*?)0+ /, '#{1} ' ).gsub(/\. /,' ');
  return text;
}

function time_ago_in_words(from_time, to_time)
{
  if(to_time == null)
    to_time = new Date();

  var from_time = from_time.valueOf();
  var to_time = to_time.valueOf();
  distance_in_seconds = Math.abs((to_time - from_time)/1000).round();
  distance_in_minutes = (distance_in_seconds/60).round();

  if(distance_in_minutes <= 1)
    return "1 minute";

  if(distance_in_minutes <= 44)
    return distance_in_minutes + " minutes";

  if(distance_in_minutes <= 89)
    return "1 hour";

  if(distance_in_minutes <= 1439)
  {
    var hours = distance_in_minutes / 60;
    hours = (hours - 0.5).round(); // round down
    return hours + " hours";
  }

  if(distance_in_minutes <= 2879)
    return "1 day";

  if(distance_in_minutes <= 43199)
  {
    var days = distance_in_minutes / 1440;
    days = (days - 0.5).round(); // round down
    return days + " days";
  }

  if(distance_in_minutes <= 86399)
    return "1 month";

  if(distance_in_minutes <= 525959)
  {
    var months = distance_in_minutes / 43200;
    months = (months - 0.5).round(); // round down
    return months + " months";
  }

  var years = (distance_in_minutes / 525960).toFixed(1);
  return years + " years";
}

scale = function(x, l1, h1, l2, h2)
{
  return ((x - l1) * (h2 - l2) / (h1 - l1) + l2);
}

clamp = function(n, min, max)
{
  return Math.max(Math.min(n, max), min);
}

var ClearNoticeTimer;
function start_notice_timer() {
  if(ClearNoticeTimer)
    window.clearTimeout(ClearNoticeTimer);

  ClearNoticeTimer = window.setTimeout(function() {
		  $('notice-container').hide();
  }, 5000);
}

var ClipRange = Class.create({
  initialize: function(min, max) {
    if (min > max)  {
      throw "paramError"
    }

    this.min = min
    this.max = max
  },

  clip: function(x) {
    if (x < this.min) {
      return this.min
    }

    if (x > this.max) {
      return this.max
    }

    return x
  }
})

Object.extend(Element, {
  appendChildBase: Element.appendChild,
  appendChild: function(e) {
    this.appendChildBase(e)
    return e
  }
});

Object.extend(Element.Methods, {
  showBase: Element.show,
  show: function(element, visible) {
    if (visible || visible == null)
      return $(element).showBase();
    else
      return $(element).hide();
  },
  setClassName: function(element, className, enabled) {
    if(enabled)
      return $(element).addClassName(className);
    else
      return $(element).removeClassName(className);
  },
  pickClassName: function(element, classNameEnabled, classNameDisabled, enabled) {
    $(element).setClassName(classNameEnabled, enabled);
    $(element).setClassName(classNameDisabled, !enabled);
  },
  isParentNode: function(element, parentNode) {
    while(element) {
      if(element == parentNode)
        return true;
      element = element.parentNode;
    }
    return false;
  },
  setTextContent: function(element, text)
  {
    if(element.innerText != null)
      element.innerText = text;
    else
      element.textContent = text;
    return element;
  },
  recursivelyVisible: function(element)
  {
    while(element != document.documentElement)
    {
      if(!element.visible())
        return false;
      element = element.parentNode;
    }
    return true;
  }
});
Element.addMethods()

var KeysDown = new Hash();

/* Many browsers eat keyup events if focus is lost while the button
 * is pressed. */
document.observe("blur", function(e) { KeysDown = new Hash(); })

function OnKeyCharCode(key, f, element)
{
  if(window.opera)
    return;
  if(!element)
    element = document;
  element.observe("keyup", function(e) {
    if (e.keyCode != key)
      return;
    KeysDown.set(KeysDown[e.keyCode], false);
  });
  element.observe("keypress", function(e) {
    if (e.charCode != key)
      return;
    if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey)
      return;
    if(KeysDown.get(KeysDown[e.keyCode]))
      return;
    KeysDown.set(KeysDown[e.keyCode], true);

    var target = e.target;
    if(target.tagName == "INPUT" || target.tagName == "TEXTAREA")
      return;

    f(e);
    e.stop();
    e.preventDefault();
  });
}

function OnKey(key, options, press, release)
{
  if(!options)
    options = {};
  var element = options["Element"]
  if(!element)
    element = document;
  if(element == document && window.opera && !options.AlwaysAllowOpera)
    return;

  element.observe("keyup", function(e) {
    if (e.keyCode != key)
      return;
    KeysDown[e.keyCode] = false
    if(release)
      release(e);
  });

  element.observe("keydown", function(e) {
    if (e.keyCode != key)
      return;
    if (e.metaKey)
      return;
    if (e.shiftKey != !!options.shiftKey)
      return;
    if (e.altKey != !!options.altKey)
      return;
    if (e.ctrlKey != !!options.ctrlKey)
      return;
    if (!options.allowRepeat && KeysDown[e.keyCode])
      return;

    KeysDown[e.keyCode] = true
    var target = e.target;
    if(!options.AllowTextAreaFields && target.tagName == "TEXTAREA")
      return;
    if(!options.AllowInputFields && target.tagName == "INPUT")
      return;

    if(press && !press(e))
      return;
    e.stop();
    e.preventDefault();
  });
}

function InitTextAreas()
{
  $$("TEXTAREA").each(function(elem) {
    var form = elem.up("FORM");
    if(!form)
      return;

    if(elem.set_login_handler)
      return;
    elem.set_login_handler = true;

    OnKey(13, { ctrlKey: true, AllowInputFields: true, AllowTextAreaFields: true, Element: elem}, function(f) {
      $(form).simulate_submit();
    });
  });
}

function InitAdvancedEditing()
{
  if(Cookie.get("show_advanced_editing") != "1")
    return;

  $(document.documentElement).removeClassName("hide-advanced-editing");
}

/* When we resume a user submit after logging in, we want to run submit events, as
 * if the submit had happened normally again, but submit() doesn't do this.  Run
 * a submit event manually. */
Element.addMethods("FORM", {
  simulate_submit: function(form)
  {
    form = $(form);

    if(document.createEvent)
    {
      var e = document.createEvent("HTMLEvents");
      e.initEvent("submit", true, true);
      form.dispatchEvent(e);

      if(!e.stopped)
        form.submit();
    }
    else
    {
      if(form.fireEvent("onsubmit"))
        form.submit();
    }
  }
});


Element.addMethods({
  simulate_anchor_click: function(a, ev)
  {
    a = $(a);

    if(document.dispatchEvent)
    {
      if(a.dispatchEvent(ev) && !ev.stopped)
        window.location.href = a.href;
    }
    else
    {
      if(a.fireEvent("onclick", ev))
        window.location.href = a.href;
    }
  }
});


clone_event = function(orig)
{
  if(document.dispatchEvent)
  {
    var e = document.createEvent("MouseEvent");
    e.initMouseEvent(orig.type, orig.canBubble, orig.cancelable, orig.view,
        orig.detail, orig.screenX, orig.screenY, orig.clientX, orig.clientY,
        orig.ctrlKey, orig.altKey, orig.shiftKey, orig.metaKey,
        orig.button, orig.relatedTarget);
    return Event.extend(e);
  }
  else
  {
    var e = document.createEventObject(orig);
    return Event.extend(e);
  }
}

Object.extend(String.prototype, {
  subst: function(subs) {
    var text = this;
    for(var s in subs)
    {
      var r = new RegExp("\\${" + s + "}", "g");
      var to = subs[s];
      if(to == null) to = "";
      text = text.replace(r, to);
    }

    return text;
  },

  createElement: function() {
    var container = document.createElement("div");
    container.innerHTML = this;
    return container.removeChild(container.firstChild);
  }
});

function createElement(type, className, html)
{
  var element = $(document.createElement(type));
  element.className = className;
  element.innerHTML = html;
  return element;
}

/* Prototype calls onSuccess instead of onFailure when the user cancelled the AJAX
 * request.  Fix that with a monkey patch, so we don't have to track changes inside
 * prototype.js. */
Ajax.Request.prototype.successBase = Ajax.Request.prototype.success;
Ajax.Request.prototype.success = function()
{
  try {
    if(this.transport.getAllResponseHeaders() == null)
      return false;
  } catch (e) {
    /* FF throws an exception if we call getAllResponseHeaders on a cancelled request. */
    return false;
  }

  return this.successBase();
}

/* Work around a Prototype bug; it discards exceptions instead of letting them fall back
 * to the browser where they'll be logged. */
Ajax.Responders.register({
  onException: function(request, exception) {
    /* Report the error here; don't wait for onerror to get it, since the exception
     * isn't passed to it so the stack trace is lost.  */
    var data = "";
    if(request.url)
      data += "AJAX URL: " + request.url + "\n";

    try {
      var params = request.parameters;
      for(key in params)
      {
        var text = params[key];
        var length = text.length;
        if(text.length > 1024)
          text = text.slice(0, 1024) + "...";
        data += "Parameter (" + length + "): " + key + "=" + text + "\n";
      }
    } catch(e) {
      data += "Couldn't get response parameters: " + e + "\n";
    }

    try {
      var text = request.transport.responseText;
      var length = text.length;
      if(text.length > 1024)
        text = text.slice(0, 1024) + "...";
      data += "Response (" + length + "): ->" + text + "<-\n";
    } catch(e) {
      data += "Couldn't get response text: " + e + "\n";
    }

    ReportError(null, null, null, exception, data);

    (function() {
      throw exception;
    }).defer();
  }
});

/*
 * In Firefox, exceptions thrown from event handlers tend to get lost.  Sometimes they
 * trigger window.onerror, but not reliably.  Catch exceptions out of event handlers and
 * throw them from a deferred context, so they'll make it up to the browser to be
 * logged.
 *
 * This depends on bindAsEventListener actually only being used for event listeners,
 * since it eats exceptions.
 *
 * Only do this in Firefox; not all browsers preserve the call stack in the exception,
 * so this can lose information if used when it's not needed.
 */
if(Prototype.Browser.Gecko)
Function.prototype.bindAsEventListener = function()
{
  var __method = this, args = $A(arguments), object = args.shift();
  return function(event) {
    try {
      return __method.apply(object, [event || window.event].concat(args));
    } catch(exception) {
      (function() { throw exception; }).defer();
    }
  }
}

window.onerror = function(error, file, line)
{
  ReportError(error, file, line, null);
}

/*
 * Return the values of list starting at idx and moving outwards.
 *
 * sort_array_by_distance([0,1,2,3,4,5,6,7,8,9], 5)
 * [5,4,6,3,7,2,8,1,9,0]
 */
sort_array_by_distance = function(list, idx)
{
  var ret = [];
  ret.push(list[idx]);
  for(var distance = 1; ; ++distance)
  {
    var length = ret.length;
    if(idx-distance >= 0)
      ret.push(list[idx-distance]);
    if(idx+distance < list.length)
      ret.push(list[idx+distance]);
    if(length == ret.length)
      break;
  }

  return ret;
}

/* Return the squared distance between two points. */
distance_squared = function(x1, y1, x2, y2)
{
  return Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2);
}

/* Return the size of the window. */
getWindowSize = function()
{
  var size = {};
  if(window.innerWidth != null)
  {
    size.width = window.innerWidth;
    size.height = window.innerHeight;
  }
  else
  {
    /* IE: */
    size.width = document.documentElement.clientWidth;
    size.height = document.documentElement.clientHeight;
  }
  return size;
}

/* If 2d canvases are supported, return one.  Otherwise, return null. */
create_canvas_2d = function()
{
  var canvas = document.createElement("canvas");
  if(canvas.getContext && canvas.getContext("2d"))
    return canvas;
  return null;
}

Prototype.Browser.AndroidWebKit = (navigator.userAgent.indexOf("Android") != -1 && navigator.userAgent.indexOf("WebKit") != -1);

/* Some UI simply doesn't make sense on a touchscreen, and may need to be disabled or changed.
 * It'd be nice if this could be done generically, but this is the best available so far ... */
Prototype.BrowserFeatures.Touchscreen = (function() {
  /* iOS WebKit has window.Touch, a constructor for Touch events. */
  if(window.Touch)
    return true;

  // Mozilla/5.0 (Linux; U; Android 2.2; en-us; sdk Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1
  if(navigator.userAgent.indexOf("Mobile Safari/") != -1)
    return true;

  // Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Mobile/8C134
  if(navigator.userAgent.indexOf("Mobile/") != -1)
    return true;

  return false;
})();

/* MI: Using this method to detect if working on mobile browser,
 * instead of using Prototype.BrowserFeatures.Touchscreen, because
 * desktop Webkit browsers have window.Touch.
 * This may not be the best method though.
 */
Prototype.BrowserIsMobile = (function() {
  var agent = navigator.userAgent;
  return !!(agent.match(/Android/i) ||
         agent.match(/BlackBerry/i) ||
         agent.match(/iPhone|iPad|iPod/i) ||
         agent.match(/Opera Mini/i) ||
         agent.match(/IEMobile/i));
})();

/* When element is dragged, the document moves around it.  If scroll_element is true, the
 * element should be positioned (eg. position: absolute), and the element itself will be
 * scrolled. */
DragElement = function(element, options)
{
  $(document.body).addClassName("not-dragging");

  this.options = options || {};
  if (!this.options.condition) this.options.condition = function() { return true }
  if(this.options.snap_pixels == null)
    this.options.snap_pixels = 10;
  this.ignore_mouse_events_until = null;

  this.mousemove_event = this.mousemove_event.bindAsEventListener(this);
  this.mousedown_event = this.mousedown_event.bindAsEventListener(this);
  this.dragstart_event = this.dragstart_event.bindAsEventListener(this);
  this.mouseup_event = this.mouseup_event.bindAsEventListener(this);
  this.click_event = this.click_event.bindAsEventListener(this);
  this.selectstart_event = this.selectstart_event.bindAsEventListener(this);

  this.touchmove_event = this.touchmove_event.bindAsEventListener(this);
  this.touchstart_event = this.touchstart_event.bindAsEventListener(this);
  this.touchend_event = this.touchend_event.bindAsEventListener(this);

  this.move_timer_update = this.move_timer_update.bind(this);

  this.element = element;
  this.dragging = false;

  this.drag_handlers = [];
  this.handlers = [];

  /*
   * Starting drag on mousedown works in most browsers, but has an annoying side-
   * effect: we need to stop the event to prevent any browser drag operations from
   * happening, and that'll also prevent clicking the element from focusing the
   * window.  Stop the actual drag in dragstart.  We won't get mousedown in
   * Opera, but we don't need to stop it there either.
   *
   * Sometimes drag events can leak through, and attributes like -moz-user-select may
   * be needed to prevent it.
   */
  if(!options.no_mouse)
  {
    this.handlers.push(element.on("mousedown", this.mousedown_event));
    this.handlers.push(element.on("dragstart", this.dragstart_event));
  }

  if(!options.no_touch)
  {
    this.handlers.push(element.on("touchstart", this.touchstart_event));
    this.handlers.push(element.on("touchmove", this.touchmove_event));
  }

  /*
   * We may or may not get a click event after mouseup.  This is a pain: if we get a
   * click event, we need to cancel it if we dragged, but we may not get a click event
   * at all; detecting whether a click event came from the drag or not is difficult.
   * Cancelling mouseup has no effect.  FF, IE7 and Opera still send the click event
   * if their dragstart or mousedown event is cancelled; WebKit doesn't.
   */
  if(!Prototype.Browser.WebKit)
    this.handlers.push(element.on("click", this.click_event));
}

DragElement.prototype.destroy = function()
{
  this.stop_dragging(null, true);
  this.handlers.each(function(h) { h.stop(); });
  this.handlers = [];
}

DragElement.prototype.move_timer_update = function()
{
  this.move_timer = null;

  if(!this.options.ondrag)
    return;

  if(this.last_event_params == null)
    return;

  var last_event_params = this.last_event_params;
  this.last_event_params = null;

  var x = last_event_params.x;
  var y = last_event_params.y;

  var anchored_x = x - this.anchor_x;
  var anchored_y = y - this.anchor_y;

  var relative_x = x - this.last_x;
  var relative_y = y - this.last_y;
  this.last_x = x;
  this.last_y = y;

  if(this.options.ondrag)
    this.options.ondrag({
      dragger: this,
      x: x,
      y: y,
      aX: anchored_x,
      aY: anchored_y,
      dX: relative_x,
      dY: relative_y,
      latest_event: last_event_params.event
    });
}

DragElement.prototype.mousemove_event = function(event)
{
  event.stop();

  var scrollLeft = (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft);
  var scrollTop = (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);

  var x = event.pointerX() - scrollLeft;
  var y = event.pointerY() - scrollTop;
  this.handle_move_event(event, x, y);
}

DragElement.prototype.touchmove_event = function(event)
{
  /* Ignore touches other than the one we started with. */
  var touch = null;
  for(var i = 0; i < event.changedTouches.length; ++i)
  {
    var t = event.changedTouches[i];
    if(t.identifier == this.dragging_touch_identifier)
    {
      touch = t;
      break;
    }
  }
  if(touch == null)
    return;

  event.preventDefault();

  /* If a touch drags over the bottom navigation bar in Safari and is released while outside of
   * the viewport, the touchend event is never sent.  Work around this by cancelling the drag
   * if we get too close to the end.  Don't do this if we're in standalone (web app) mode, since
   * there's no navigation bar. */
  if(!window.navigator.standalone && touch.pageY > window.innerHeight-10)
  {
    debug("Dragged off the bottom");
    this.stop_dragging(event, true);
    return;
  }

  var x = touch.pageX;
  var y = touch.pageY;

  this.handle_move_event(event, x, y);
}

DragElement.prototype.handle_move_event = function(event, x, y)
{
  if(!this.dragging)
    return;

  if(!this.dragged)
  {
    var distance = Math.pow(x - this.anchor_x, 2) + Math.pow(y - this.anchor_y, 2);
    var snap_pixels = this.options.snap_pixels;
    snap_pixels *= snap_pixels;

    if(distance < snap_pixels) // 10 pixels
      return;
  }

  if(!this.dragged)
  {
    if(this.options.onstartdrag)
    {
      /* Call the onstartdrag callback.  If it returns true, cancel the drag. */
      if(this.options.onstartdrag({ handler: this, latest_event: event }))
      {
        this.dragging = false;
        return;
      }
    }

    this.dragged = true;

    $(document.body).addClassName(this.overriden_drag_class || "dragging");
    $(document.body).removeClassName("not-dragging");
  }

  this.last_event_params = {
    x: x,
    y: y,
    event: event
  };

  if(this.dragging_by_touch && Prototype.Browser.AndroidWebKit)
  {
    /* Touch events on Android tend to queue up when they come in faster than we
     * can process.  Set a timer, so we discard multiple events in quick succession. */
    if(this.move_timer == null)
      this.move_timer = window.setTimeout(this.move_timer_update, 10);
  }
  else
  {
    this.move_timer_update();
  }
}

DragElement.prototype.mousedown_event = function(event)
{
  if(!event.isLeftClick())
    return;

  /* Check if we're temporarily ignoring mouse events. */
  if(this.ignore_mouse_events_until != null)
  {
    var now = (new Date()).valueOf();
    if(now < this.ignore_mouse_events_until)
      return;

    this.ignore_mouse_events_until = null;
  }
  var scrollLeft = (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft);
  var scrollTop = (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
  var x = event.pointerX() - scrollLeft;
  var y = event.pointerY() - scrollTop;

  this.start_dragging(event, false, x, y, 0);
}

DragElement.prototype.touchstart_event = function(event)
{
  /* If we have multiple touches, find the first one that actually refers to us. */
  var touch = null;
  for(var i = 0; i < event.changedTouches.length; ++i)
  {
    var t = event.changedTouches[i];
    if(!t.target.isParentNode(this.element))
      continue;
    touch = t;
    break;
  }
  if(touch == null)
    return;

  var x = touch.pageX;
  var y = touch.pageY;

  this.start_dragging(event, true, x, y, touch.identifier);
}

DragElement.prototype.start_dragging = function(event, touch, x, y, touch_identifier)
{
  if(this.dragging_touch_identifier != null || (this.options.condition && this.options.condition() === false))
    return;

  /* If we've been started with a touch event, only listen for touch events.  If we've
   * been started with a mouse event, only listen for mouse events.  We may receive
   * both sets of events, and the anchor coordinates for the two may not be compatible. */
  this.drag_handlers.push(document.on("selectstart", this.selectstart_event));
  this.drag_handlers.push(Element.on(window, "pagehide", this.pagehide_event.bindAsEventListener(this)));
  if(touch)
  {
    this.drag_handlers.push(document.on("touchend", this.touchend_event));
    this.drag_handlers.push(document.on("touchcancel", this.touchend_event));
    this.drag_handlers.push(document.on("touchmove", this.touchmove_event));
  }
  else
  {
    this.drag_handlers.push(document.on("mouseup", this.mouseup_event));
    this.drag_handlers.push(document.on("mousemove", this.mousemove_event));
  }

  this.dragging = true;
  this.dragged = false;
  this.dragging_by_touch = touch;
  this.dragging_touch_identifier = touch_identifier;

  this.anchor_x = x;
  this.anchor_y = y;
  this.last_x = this.anchor_x;
  this.last_y = this.anchor_y;

  if(this.options.ondown)
    this.options.ondown({
      dragger: this,
      x: x,
      y: y,
      latest_event: event
    });
}

DragElement.prototype.pagehide_event = function(event)
{
  this.stop_dragging(event, true);
}

DragElement.prototype.touchend_event = function(event)
{
  /* If our touch was released, stop the drag. */
  for(var i = 0; i < event.changedTouches.length; ++i)
  {
    var t = event.changedTouches[i];
    if(t.identifier == this.dragging_touch_identifier)
    {
      this.stop_dragging(event, event.type == "touchcancel");

      /*
       * Work around a bug on iPhone.  The mousedown and mouseup events are sent after
       * the touch is released, instead of when they should be (immediately following
       * touchstart and touchend).  This means we'll process each touch as a touch,
       * then immediately after as a mouse press, and fire ondown/onup events for each.
       *
       * We can't simply ignore mouse presses if touch events are supported; some devices
       * will support both touches and mice and both types of events will always need to
       * be handled.
       *
       * After a touch is released, ignore all mouse presses for a little while.  It's
       * unlikely that the user will touch an element, then immediately click it.
       */
      this.ignore_mouse_events_until = (new Date()).valueOf() + 500;
      return;
    }
  }
}

DragElement.prototype.mouseup_event = function(event)
{
  if(!event.isLeftClick())
    return;

  this.stop_dragging(event, false);
}

/* If cancelling is true, we're stopping for a reason other than an explicit mouse/touch
 * release. */
DragElement.prototype.stop_dragging = function(event, cancelling)
{
  if(this.dragging)
  {
    this.dragging = false;
    $(document.body).removeClassName(this.overriden_drag_class || "dragging");
    $(document.body).addClassName("not-dragging");

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

  this.drag_handlers.each(function(h) { h.stop(); });
  this.drag_handlers = [];
  this.dragging_touch_identifier = null;

  if(this.options.onup)
    this.options.onup({
      dragger: this,
      latest_event: event,
      cancelling: cancelling
    });
}

DragElement.prototype.click_event = function(event)
{
  /* If this click was part of a drag, cancel the click. */
  if(this.dragged)
    event.stop();
  this.dragged = false;
}

DragElement.prototype.dragstart_event = function(event)
{
  event.preventDefault();
}

DragElement.prototype.selectstart_event = function(event)
{
  /* We need to stop selectstart to prevent drag selection in Chrome.  However, we need
   * to work around a bug: if we stop the event of an INPUT element, it'll prevent focusing
   * on that element entirely.  We shouldn't prevent selecting the text in the input box,
   * either. */
  if(event.target.tagName != "INPUT")
    event.stop();
}

/* When element is dragged, the document moves around it.  If scroll_element is true, the
 * element should be positioned (eg. position: absolute), and the element itself will be
 * scrolled. */
WindowDragElement = function(element, extra_opts)
{
  this.element = element;
  /* MI: Changed the way startdrag is called so
   * additional onstartdrag callbacks can be passed.
   */
  var startdrag = this.startdrag.bind(this);
  var extra_startdrag = extra_opts.startdrag || false;
  this.dragger = new DragElement(element, jQuery.extend({}, extra_opts || {}, {
    no_touch: true,
    ondrag: this.ondrag.bind(this),
    onstartdrag: function() {
      startdrag();
      if (extra_startdrag)
        extra_startdrag();
    },
  }));
}

WindowDragElement.prototype.startdrag = function()
{
  this.scroll_anchor_x = (window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft);
  this.scroll_anchor_y = (window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
}

WindowDragElement.prototype.ondrag = function(e)
{
  var scrollLeft = this.scroll_anchor_x - e.aX;
  var scrollTop = this.scroll_anchor_y - e.aY;
  scrollTo(scrollLeft, scrollTop);
}

/* element should be positioned (eg. position: absolute).  When the element is dragged,
 * scroll it around. */
WindowDragElementAbsolute = function(element, ondrag_callback)
{
  this.element = element;
  this.ondrag_callback = ondrag_callback;
  this.disabled = false;
  this.dragger = new DragElement(element, {
    ondrag: this.ondrag.bind(this),
    onstartdrag: this.startdrag.bind(this)
  });
}

WindowDragElementAbsolute.prototype.set_disabled = function(b) { this.disabled = b; }

WindowDragElementAbsolute.prototype.startdrag = function()
{
  if(this.disabled)
    return true; /* cancel */

  this.scroll_anchor_x = this.element.offsetLeft;
  this.scroll_anchor_y = this.element.offsetTop;
  return false;
}

WindowDragElementAbsolute.prototype.ondrag = function(e)
{
  var scrollLeft = this.scroll_anchor_x + e.aX;
  var scrollTop = this.scroll_anchor_y + e.aY;

  /* Don't allow dragging the image off the screen; there'll be no way to
   * get it back. */
  var window_size = getWindowSize();
  var min_visible = Math.min(100, this.element.offsetWidth);
  scrollLeft = Math.max(scrollLeft, min_visible - this.element.offsetWidth);
  scrollLeft = Math.min(scrollLeft, window_size.width - min_visible);

  var min_visible = Math.min(100, this.element.offsetHeight);
  scrollTop = Math.max(scrollTop, min_visible - this.element.offsetHeight);
  scrollTop = Math.min(scrollTop, window_size.height - min_visible);
  this.element.setStyle({left: scrollLeft + "px", top: scrollTop + "px"});

  if(this.ondrag_callback)
    this.ondrag_callback();
}

WindowDragElementAbsolute.prototype.destroy = function()
{
  this.dragger.destroy();
}

/* Track the focused element, and store it in document.focusedElement.. */
function TrackFocus()
{
  document.focusedElement = null;
  if(document.addEventListener)
  {
    document.addEventListener("focus", function(e)
    {
      document.focusedElement = e.target;
    }.bindAsEventListener(this), true);
  }
  document.observe("focusin", function(event) {
    document.focusedElement = event.srcElement;
  }.bindAsEventListener(this));
}

function FormatError(message, file, line, exc, info)
{
  var report = "";
  report += "Error: " + message + "\n";

  if(info != null)
    report += info;

  report += "UA: " + window.navigator.userAgent + "\n";
  report += "URL: " + window.location.href + "\n";

  var cookies = document.cookie;
  cookies = cookies.replace(/(pass_hash)=[0-9a-f]{40}/, "$1=(removed)");
  try {
    report += "Cookies: " + decodeURIComponent(cookies) + "\n";
  } catch(e) {
    report += "Cookies (couldn't decode): " + cookies + "\n";
  }

  if("localStorage" in window)
  {
    /* FF's localStorage is broken; we can't iterate over it.  Name the keys we use explicitly. */
    var keys = [];
    try {
      for(key in localStorage)
        keys.push(keys);
    } catch(e) {
      keys = ["sample_urls", "sample_url_fifo", "tag_data", "tag_data_version", "recent_tags", "tag_data_format"];
    }

    for(var i = 0; i < keys.length; ++i)
    {
      var key = keys[i];
      try {
        if(!(key in localStorage))
          continue;

        var data = localStorage[key];
        var length = data.length;
        if(data.length > 512)
          data = data.slice(0, 512);

        report += "localStorage." + key + " (size: " + length + "): " + data + "\n";
      } catch(e) {
        report += "(ignored errors retrieving localStorage for " + key + ": " + e + ")\n";
      }
    }
  }

  if(exc && exc.stack)
    report += "\n" + exc.stack + "\n";

  if(file)
  {
    report += "File: " + file;
    if(line != null)
      report += " line " + line + "\n";
  }

  return report;
}

var reported_error = false;
function ReportError(message, file, line, exc, info)
{
  if(navigator.userAgent.match(/.*MSIE [67]/))
    return;

  /* Only attempt to report an error once per page load. */
  if(reported_error)
    return;
  reported_error = true;

  /* Only report an error at most once per hour. */
  if(document.cookie.indexOf("reported_error=1") != -1)
    return;

  var expiration = new Date();
  expiration.setTime(expiration.getTime() + (60 * 60 * 1000));
  document.cookie = "reported_error=1; path=/; expires=" + expiration.toGMTString();

  var report = FormatError(exc? exc.message:message, file, line, exc, info);

  try {
    new Ajax.Request("/user/error.json", {
      parameters: { report: report }
    });
  } catch(e) {
    alert("Error: " + e);
  }
}

function LocalStorageDisabled()
{
  if(!("localStorage" in window))
    return "unsupported";

  var cleared_storage = false;
  while(1)
  {
    try {
      /* We can't just access a property to test it; that detects it being disabled in FF, but
       * not in Chrome. */
      localStorage.x = 1;
      if(localStorage.x != 1)
        throw "disabled";
      delete localStorage.x;
      return null;
    } catch(e) {
      /* If local storage is full, we may not be able to even run this test.  If that ever happens
       * something is wrong, so after a failure clear localStorage once and try again.  This call
       * may fail, too; ignore that and we'll catch the problem on the next try. */
      if(!cleared_storage)
      {
        cleared_storage = true;
        try {
          localStorage.clear();
        } catch(e) { }
        continue;
      }
      if(navigator.userAgent.indexOf("Gecko/") != -1)
      {
        // If the user or an extension toggles about:config dom.storage.enabled, this happens:
        if(e.message.indexOf("Security error") != -1)
          return "ff-disabled";
      }

      /* Chrome unhelpfully reports QUOTA_EXCEEDED_ERR if local storage is disabled, which
       * means we can't easily detect it being disabled and show a tip to the user. */
      return "error";
    }
  }
}

/* Chrome 10/WebKit braindamage; stop breaking things intentionally just to create
 * busywork for everyone else: */
if(!("URL" in window) && "webkitURL" in window)
  window.URL = window.webkitURL;

/* For Chrome 9: */
if("createObjectURL" in window && !("URL" in window))
{
  window.URL = {
    createObjectURL: function(blob) { return window.createObjectURL(blob); },
    revokeObjectURL: function(url) { window.revokeObjectURL(url); }
  }
}

/* Allow CSS styles for WebKit. */
if(navigator.userAgent.indexOf("AppleWebKit/") != -1)
  document.documentElement.className += " webkit";

