define(["leaflet", "rbush"],
  function (L, rbush) {
    var labelLocations = [["left",   "middle",      0 / 8],
                          ["center", "top",         6 / 8],
                          ["right",  "middle",      4 / 8],
                          ["left",   "top",         7 / 8],
                          ["left",   "ideographic", 1 / 8],
                          ["right",  "top",         5 / 8],
                          ["center", "ideographic", 2 / 8],
                          ["right",  "ideographic", 3 / 8]]

    var fontFamily = "Roboto"
    var nodeRadius = 4

    var ctx = document.createElement("canvas").getContext("2d")

    function measureText(font, text) {
      ctx.font = font
      return ctx.measureText(text)
    }

    function mapRTree(d) {
      var o = [d.position.lat, d.position.lng, d.position.lat, d.position.lng]

      o.label = d

      return o
    }

    function prepareLabel(fillStyle, fontSize, offset, stroke, minZoom) {
      return function (d) {
        var font = fontSize + "px " + fontFamily
        return { position: L.latLng(d.nodeinfo.location.latitude, d.nodeinfo.location.longitude),
                 label: d.nodeinfo.hostname,
                 offset: offset,
                 fillStyle: fillStyle,
                 height: fontSize * 1.2,
                 font: font,
                 stroke: stroke,
                 minZoom: minZoom,
                 width: measureText(font, d.nodeinfo.hostname).width
               }
      }
    }

    function calcOffset(offset, loc) {
      return [ offset * Math.cos(loc[2] * 2 * Math.PI),
              -offset * Math.sin(loc[2] * 2 * Math.PI)]
    }

    function labelRect(p, offset, anchor, label, minZoom, maxZoom, z) {
      var margin = 1 + 1.41 * (1 - (z - minZoom) / (maxZoom - minZoom))

      var width = label.width * margin
      var height = label.height * margin

      var dx = { left: 0,
                 right: -width,
                 center: -width /  2
               }

      var dy = { top: 0,
                 ideographic: -height,
                 middle: -height / 2
               }

      var x = p.x + offset[0] + dx[anchor[0]]
      var y = p.y + offset[1] + dy[anchor[1]]

      return [x, y, x + width, y + height]
    }

    var c = L.TileLayer.Canvas.extend({
      onAdd: function (map) {
        L.TileLayer.Canvas.prototype.onAdd.call(this, map)
        if (this.data)
          this.prepareLabels()
      },
      setData: function (d) {
        this.data = d
        if (this._map)
          this.prepareLabels()
      },
      prepareLabels: function () {
        var d = this.data

        // label:
        // - position (WGS84 coords)
        // - offset (2D vector in pixels)
        // - anchor (tuple, textAlignment, textBaseline)
        // - minZoom (inclusive)
        // - label (string)
        // - color (string)

        var labelsOnline = d.online.map(prepareLabel("rgba(0, 0, 0, 0.9)", 10, 8, true, 13))
        var labelsOffline = d.offline.map(prepareLabel("rgba(212, 62, 42, 0.9)", 9, 5, false, 16))
        var labelsNew = d.new.map(prepareLabel("rgba(48, 99, 20, 0.9)", 11, 8, true, 0))
        var labelsLost = d.lost.map(prepareLabel("rgba(212, 62, 42, 0.9)", 11, 8, true, 0))

        var labels = []
                     .concat(labelsNew)
                     .concat(labelsLost)
                     .concat(labelsOnline)
                     .concat(labelsOffline)

        var minZoom = this.options.minZoom
        var maxZoom = this.options.maxZoom

        var trees = []

        var map = this._map

        function nodeToRect(z) {
          return function (d) {
            var p = map.project(d.position, z)
            return [p.x - nodeRadius, p.y - nodeRadius,
                    p.x + nodeRadius, p.y + nodeRadius]
          }
        }

        for (var z = minZoom; z <= maxZoom; z++) {
          trees[z] = rbush(9)
          trees[z].load(labels.map(nodeToRect(z)))
        }

        labels = labels.map(function (d) {
          var best = labelLocations.map(function (loc) {
            var offset = calcOffset(d.offset, loc)
            var z

            for (z = maxZoom; z >= d.minZoom; z--) {
              var p = map.project(d.position, z)
              var rect = labelRect(p, offset, loc, d, minZoom, maxZoom, z)
              var candidates = trees[z].search(rect)

              if (candidates.length > 0)
                break
            }

            return {loc: loc, z: z + 1}
          }).filter(function (d) {
            return d.z <= maxZoom
          }).sort(function (a, b) {
            return a.z - b.z
          })[0]

          if (best !== undefined) {
            d.offset = calcOffset(d.offset, best.loc)
            d.minZoom = best.z
            d.anchor = best.loc

            for (var z = maxZoom; z >= best.z; z--) {
              var p = map.project(d.position, z)
              var rect = labelRect(p, d.offset, best.loc, d, minZoom, maxZoom, z)
              trees[z].insert(rect)
            }

            return d
          } else
            return undefined
        }).filter(function (d) { return d !== undefined })

        this.margin = 16

        if (labels.length > 0)
          this.margin += labels.map(function (d) {
            return d.width
          }).sort().reverse()[0]

        this.labels = rbush(9)
        this.labels.load(labels.map(mapRTree))

        this.redraw()
      },
      drawTile: function (canvas, tilePoint, zoom) {
        function getTileBBox(s, map, tileSize, margin) {
          var tl = map.unproject([s.x - margin, s.y - margin])
          var br = map.unproject([s.x + margin + tileSize, s.y + margin + tileSize])

          return [br.lat, tl.lng, tl.lat, br.lng]
        }

        if (!this.labels)
          return

        var tileSize = this.options.tileSize
        var s = tilePoint.multiplyBy(tileSize)
        var map = this._map

        function projectNodes(d) {
          var p = map.project(d.label.position)

          p.x -= s.x
          p.y -= s.y

          return {p: p, label: d.label}
        }

        var bbox = getTileBBox(s, map, tileSize, this.margin)

        var labels = this.labels.search(bbox).map(projectNodes)

        var ctx = canvas.getContext("2d")

        ctx.lineWidth = 5
        ctx.strokeStyle = "rgba(255, 255, 255, 0.8)"
        ctx.miterLimit = 2

        function drawLabel(d) {
          ctx.font = d.label.font
          ctx.textAlign = d.label.anchor[0]
          ctx.textBaseline = d.label.anchor[1]
          ctx.fillStyle = d.label.fillStyle

          if (d.label.stroke)
            ctx.strokeText(d.label.label, d.p.x + d.label.offset[0], d.p.y + d.label.offset[1])

          ctx.fillText(d.label.label, d.p.x + d.label.offset[0], d.p.y + d.label.offset[1])
        }

        labels.filter(function (d) {
          return zoom >= d.label.minZoom
        }).forEach(drawLabel)
      }
    })

    return c
})