function QuoteSet(location, lat, lng, id) {
  this.location = location;
  this.lat = lat;
  this.lng = lng;
  this.id = id;
  this.point = new GLatLng(this.lat, this.lng);
  this.iconNode = null;
  this.infoWindowNode = null;
}

QuoteSet.prototype.getPoint = function() {
  return this.point;
}

QuoteSet.prototype.getIconNode = function() {
  if (!this.iconNode)
  {
    this.iconNode = newNode("div");
    var className = "marker";
    this.iconNode.className = className;
    this.iconNode.quoteSet = this;
    this.iconNode.setAttribute('tiptitle', this.location);
	this.iconNode.onmouseover = function() {
      tooltip.show(this.getAttribute('tiptitle'));
	};
	this.iconNode.onmouseout = function() {tooltip.hide()};
  }

  return this.iconNode;
}

QuoteSet.prototype.getIconNodeBounds = function() {
  var tl = new GPoint(this.iconNode.offsetLeft, this.iconNode.offsetTop);
  var br = new GPoint(tl.x + this.iconNode.offsetWidth, tl.y + this.iconNode.offsetHeight);

  return new GBounds([tl, br]);
}

QuoteSet.prototype.getQuotes = function() {
  if (this.matchedSearch) {
    return this.matchingQuotes;
  } else {
    return this.quotes;
  }
}

QuoteSet.prototype.showInfoWindow = function() {
  map.closeInfoWindow();
  map.openInfoWindow(this.point,
                     this.getInfoWindowNode());
}

QuoteSet.prototype.getInfoWindowNode = function() {
  if (!this.infoWindowNode) {
    this.infoWindowNode = newNode("iframe");
	this.infoWindowNode.src = "popupreview.php?Pub=" + this.id;
	this.infoWindowNode.frameBorder = 0;
  }

  if (!map.getInfoWindow().isHidden()) {
      this.infoWindowNode.style.display = "none";
      map.openInfoWindow(this.point,
                         this.infoWindowNode);
      this.infoWindowNode.style.display = "";
   }

  return this.infoWindowNode;
}

function QuotesOverlay(quotes) {
  GOverlay.call(this);

  this.map = null;
  this.quotes = quotes;
  this.visibleQuotes = {};
}

QuotesOverlay.prototype.initialize = function(map) {
  this.map = map;
  this.parentNode = map.getPane(G_MAP_MARKER_PANE);

  // To handle clicks in the shadow, adding a simple handler to
  // G_MAP_MARKER_MOUSE_TARGET_PANE doesn't seem to work, and we don't want
  // to have to add DOM nodes there too. So instead we add a global click
  // handler to the map that looks for clicked markers if the regular
  // handler doesn't trigger
  GEvent.bindDom(this.parentNode, "click", this, this.handleDomClick);
  GEvent.bind(map, "click", this, this.handleMapClick);
  GEvent.bind(map, "dragstart", this, this.beginMapDrag);
  GEvent.bind(map, "dragend", this, this.endMapDrag);
}

QuotesOverlay.prototype.beginMapDrag = function() {
  // Don't count short drags, so that they still trigger marker clicks
  var self = this;
  this.beginDragTimeout = window.setTimeout(function() {
    self.beginDragTimeout = null;
    self.inDrag = true;
  }, 250);
}

QuotesOverlay.prototype.endMapDrag = function() {
  if (this.beginDragTimeout) {
    window.clearTimeout(this.beginDragTimeout);
  } else {
    // We reset the drag state in a timeout because we want the click event
    // (if any) to be processed first
    var self = this;
    window.setTimeout(function() {
      self.inDrag = false;
    }, 0);
  }
}

QuotesOverlay.prototype.handleMapClick = function(marker, point) {
  var self = this;
  this.handledClick = false;
  window.setTimeout(function() {
    if (self.handledClick) return;

    if (point != null)
    {
      var domPoint = map.fromLatLngToDivPixel(point);
      var domBounds = new GBounds([domPoint]);

      for (var location in self.visibleQuotes) {
        var quote = self.visibleQuotes[location];

        if (quote.getIconNodeBounds().containsBounds(domBounds)) {
          self.handleDomClick({target: quote.getIconNode()});
          break;
        }
      }
    }
  }, 0);
}

QuotesOverlay.prototype.handleDomClick = function(event) {
  this.handledClick = true;

  if (this.inDrag) {
    return;
  }

  for (var node = event.target; node; node = node.parentNode) {
    if (node.quoteSet) {
      node.quoteSet.showInfoWindow();
      break;
    }
  }
}

QuotesOverlay.prototype.remove = function() {
  window.console.log("TODO: removing");
}

QuotesOverlay.prototype.copy = function() {
  window.console.log("TODO: copying");

  return this;
}

QuotesOverlay.prototype.redraw = function(force) {
  if (force) {
    var zoom = map.getZoom();

    addClass(containerNode, "quotes");

    this.resetVisibleQuotes();
  } else {
    if (this.updateVisibleQuotesTimeout) {
      window.clearTimeout(this.updateVisibleQuotesTimeout);
    }
    var self = this;
    this.updateVisibleQuotesTimeout = window.setTimeout(function() {
      self.updateVisibleQuotesTimeout = null;
      self.updateVisibleQuotes();
    }, 100);
  }
}

QuotesOverlay.prototype.resetVisibleQuotes = function() {
  for (var location in this.visibleQuotes) {
    var quote = this.visibleQuotes[location];
    this.parentNode.removeChild(quote.getIconNode());
  }

  this.visibleQuotes = {};

  this.updateVisibleQuotes();
}

QuotesOverlay.prototype.updateVisibleQuotes = function() {
  var start = new Date().getTime();

  var quotesToRemove = [];
  var quotesToAdd = [];
  var newVisibleQuotes = {};
  var mapBounds = this.map.getBounds();

  // Enlarge bounds a bit so points at the edges don't flicker in and out
  var sw = mapBounds.getSouthWest();
  var ne = mapBounds.getNorthEast();

  sw = new GLatLng(sw.lat() - 0.001, sw.lng() - 0.001);
  ne = new GLatLng(ne.lat() + 0.001, ne.lng() + 0.001);

  mapBounds = new GLatLngBounds(sw, ne);

  zoom = map.getZoom();

  visibleCount = 0;
  for (var i = 0, quoteSet; quoteSet = this.quotes[i]; i++) {
    var alreadyVisible = quoteSet.location in this.visibleQuotes;

    if (mapBounds.contains(quoteSet.getPoint()))
	{
      visibleCount++;
      newVisibleQuotes[quoteSet.location] = quoteSet;
      if (!alreadyVisible)
	  {
        quotesToAdd.push(quoteSet);
      }
    }
	else if (alreadyVisible)
	{
      quotesToRemove.push(quoteSet);
    }
  }

  var removeStart = new Date().getTime();

  for (var i = 0, quoteSet; quoteSet = quotesToRemove[i]; i++) {
    this.parentNode.removeChild(quoteSet.getIconNode());
  }

  this.visibleQuotes = newVisibleQuotes;

  for (var i = 0, quoteSet; quoteSet = quotesToAdd[i]; i++) {
    var iconNode = quoteSet.getIconNode();
    var iconPosition = this.map.fromLatLngToDivPixel(quoteSet.getPoint());

    iconNode.style.left = iconPosition.x + "px";
    iconNode.style.top = iconPosition.y + "px";

    this.parentNode.appendChild(iconNode);
  }
}