interface Score {
  lat: number
  lon: number
  score: number
}

interface Station {
  lat: number
  lon: number
  label: string
}

interface ColorScaleEntry {
  min: number
  r: number
  g: number
  b: number
}

interface RGB {
  r: number
  g: number
  b: number
}

interface RoverMapHook extends ViewHook {
  stations: Station[]
  stationMarkers: L.LayerGroup
  scoreOverlay: L.GridLayer | null
  gridLookup: Map<string, number>
  colorScale: ColorScaleEntry[]
  map: L.Map
  pushBounds: (this: RoverMapHook) => void
  visibilityHandler: (() => void) | null
  renderScores(this: RoverMapHook, scores: Score[]): void
  interpolateScore(this: RoverMapHook, lat: number, lon: number, step: number): number | null
  scoreColorRGB(this: RoverMapHook, score: number): RGB
  renderStations(this: RoverMapHook): void
  fitBounds(this: RoverMapHook): void
  reconnected(this: RoverMapHook): void
  destroyed(this: RoverMapHook): void
}

export const RoverMap: RoverMapHook = {
  mounted(this: RoverMapHook) {
    this.stations = []
    this.stationMarkers = L.layerGroup()
    this.scoreOverlay = null
    this.gridLookup = new Map()

    this.colorScale = [
      { min: 80, r: 0, g: 255, b: 163 },
      { min: 65, r: 125, g: 255, b: 212 },
      { min: 50, r: 255, g: 229, b: 102 },
      { min: 33, r: 255, g: 144, b: 68 },
      { min: 0,  r: 255, g: 79,  b: 79 }
    ]

    const map = L.map(this.el, {
      center: [33.2, -97.1],
      zoom: 7,
      zoomControl: true,
      attributionControl: false
    })

    L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
      maxZoom: 19
    }).addTo(map)

    this.map = map
    this.stationMarkers.addTo(map)

    // Render pre-loaded propagation scores
    const initialScores: Score[] = JSON.parse(this.el.dataset.scores || "[]")
    if (initialScores.length > 0) {
      this.renderScores(initialScores)
    }

    // Report bounds so server can send scores for visible area
    this.pushBounds = () => {
      const b = map.getBounds()
      this.pushEvent("map_bounds", {
        south: b.getSouth(), north: b.getNorth(),
        west: b.getWest(), east: b.getEast()
      })
    }

    map.on("moveend", () => {
      this.pushBounds()
    })

    this.handleEvent("update_scores", ({ scores }: { scores: Score[] }) => {
      this.renderScores(scores)
    })

    this.handleEvent("stations_updated", ({ stations }: { stations: Station[] }) => {
      this.stations = stations
      this.renderStations()
    })

    // Refresh overlay when the tab returns to visibility. Tiles created while
    // hidden paint blank if the retained gridLookup no longer covers the
    // current viewport; re-fetching bounds triggers update_scores and a full
    // overlay rebuild.
    this.visibilityHandler = () => {
      if (document.visibilityState === "visible") {
        this.map.invalidateSize()
        this.pushBounds()
      }
    }
    document.addEventListener("visibilitychange", this.visibilityHandler)
  },

  reconnected(this: RoverMapHook) {
    if (this.map) {
      this.map.invalidateSize()
      this.pushBounds()
    }
  },

  destroyed(this: RoverMapHook) {
    if (this.visibilityHandler) {
      document.removeEventListener("visibilitychange", this.visibilityHandler)
      this.visibilityHandler = null
    }
  },

  // --- Propagation score overlay (same as main map) ---

  renderScores(this: RoverMapHook, scores: Score[]) {
    if (this.scoreOverlay) {
      this.map.removeLayer(this.scoreOverlay)
      this.scoreOverlay = null
    }
    if (!scores || scores.length === 0) return

    this.gridLookup = new Map()
    scores.forEach((s) => {
      const key = `${s.lat.toFixed(3)},${s.lon.toFixed(3)}`
      this.gridLookup.set(key, s.score)
    })

    const colorCache = new Array<string>(101)
    for (let i = 0; i <= 100; i++) {
      const c = this.scoreColorRGB(i)
      colorCache[i] = `rgba(${c.r},${c.g},${c.b},0.55)`
    }

    const self = this
    const OverlayClass = L.GridLayer.extend({
      createTile(coords: L.Coords) {
        const tile = document.createElement("canvas")
        const size = this.getTileSize()
        tile.width = size.x
        tile.height = size.y
        const ctx = tile.getContext("2d")!

        const step = 0.125
        const nwPoint = coords.scaleBy(size)
        const nw = this._map.unproject(nwPoint, coords.z)
        const se = this._map.unproject(nwPoint.add(size), coords.z)

        const latPerPx = (nw.lat - se.lat) / size.y
        const lonPerPx = (se.lng - nw.lng) / size.x
        const cellPxX = Math.max(1, Math.ceil(step / lonPerPx))
        const cellPxY = Math.max(1, Math.ceil(step / Math.abs(latPerPx)))
        const absLatPerPx = Math.abs(latPerPx)

        let lastColor: string | null = null
        for (let py = 0; py < size.y; py += cellPxY) {
          const lat = nw.lat - py * absLatPerPx
          for (let px = 0; px < size.x; px += cellPxX) {
            const lon = nw.lng + px * lonPerPx
            const score = self.interpolateScore(lat, lon, step)
            if (score !== null) {
              const color = colorCache[Math.max(0, Math.min(100, Math.round(score)))]
              if (color !== lastColor) {
                ctx.fillStyle = color
                lastColor = color
              }
              ctx.fillRect(px, py, cellPxX, cellPxY)
            }
          }
        }
        return tile
      }
    })

    this.scoreOverlay = new OverlayClass({ opacity: 1.0 }) as L.GridLayer
    this.scoreOverlay!.addTo(this.map)
  },

  interpolateScore(this: RoverMapHook, lat: number, lon: number, step: number): number | null {
    const rlat = (Math.round(lat / step) * step).toFixed(3)
    const rlon = (Math.round(lon / step) * step).toFixed(3)
    if (this.gridLookup.has(`${rlat},${rlon}`)) return this.gridLookup.get(`${rlat},${rlon}`)!

    const lat0 = Math.floor(lat / step) * step
    const lat1 = lat0 + step
    const lon0 = Math.floor(lon / step) * step
    const lon1 = lon0 + step

    const s00 = this.gridLookup.get(`${lat0.toFixed(3)},${lon0.toFixed(3)}`)
    const s10 = this.gridLookup.get(`${lat1.toFixed(3)},${lon0.toFixed(3)}`)
    const s01 = this.gridLookup.get(`${lat0.toFixed(3)},${lon1.toFixed(3)}`)
    const s11 = this.gridLookup.get(`${lat1.toFixed(3)},${lon1.toFixed(3)}`)

    const corners = [s00, s10, s01, s11].filter((s): s is number => s !== undefined)
    if (corners.length < 2) return null

    if (corners.length === 4) {
      const tLat = (lat - lat0) / step
      const tLon = (lon - lon0) / step
      return Math.round(
        s00! * (1 - tLat) * (1 - tLon) + s10! * tLat * (1 - tLon) +
        s01! * (1 - tLat) * tLon + s11! * tLat * tLon
      )
    }
    return Math.round(corners.reduce((a, b) => a + b, 0) / corners.length)
  },

  scoreColorRGB(this: RoverMapHook, score: number): RGB {
    for (let i = 0; i < this.colorScale.length - 1; i++) {
      const upper = this.colorScale[i]
      const lower = this.colorScale[i + 1]
      if (score >= lower.min) {
        const range = upper.min - lower.min
        const t = Math.min(1, (score - lower.min) / range)
        return {
          r: Math.round(lower.r + (upper.r - lower.r) * t),
          g: Math.round(lower.g + (upper.g - lower.g) * t),
          b: Math.round(lower.b + (upper.b - lower.b) * t)
        }
      }
    }
    const last = this.colorScale[this.colorScale.length - 1]
    return { r: last.r, g: last.g, b: last.b }
  },

  // --- Station rendering ---

  renderStations(this: RoverMapHook) {
    this.stationMarkers.clearLayers()

    for (const s of this.stations) {
      const marker = L.circleMarker([s.lat, s.lon], {
        radius: 7,
        color: "#fff",
        weight: 2,
        fillColor: "#3b82f6",
        fillOpacity: 0.9,
        interactive: true,
        pane: "markerPane"
      })

      marker.bindTooltip(s.label, {
        permanent: true,
        direction: "top",
        offset: [0, -10],
        className: "station-label"
      })

      this.stationMarkers.addLayer(marker)
    }

    this.fitBounds()
  },

  fitBounds(this: RoverMapHook) {
    const points: [number, number][] = this.stations.map(s => [s.lat, s.lon])
    if (points.length >= 2) {
      this.map.fitBounds(points, { padding: [60, 60], maxZoom: 9 })
    } else if (points.length === 1) {
      this.map.setView(points[0], 8)
    }
  }
} as unknown as RoverMapHook
