/**
 * TileTextureManager — loads satellite tiles from a CDN and composites
 * them onto a single equirectangular canvas for use as a globe.gl texture.
 *
 * Tiles are standard Web Mercator (z/x/y). The manager handles the
 * Mercator → equirectangular reprojection when painting onto the canvas.
 */

// Proxy through our server (CDN requires Referer: radio.garden)
const TILE_CDN = '/api/radio/tile';
const FALLBACK_TEXTURE = '/earth-night.jpg';

// ── Mercator math ──

function tileToLng(x: number, z: number): number {
  return (x / (1 << z)) * 360 - 180;
}

function tileToLat(y: number, z: number): number {
  const n = Math.PI - (2 * Math.PI * y) / (1 << z);
  return (180 / Math.PI) * Math.atan(Math.sinh(n));
}

function latToTileY(lat: number, z: number): number {
  const r = (lat * Math.PI) / 180;
  return ((1 - Math.log(Math.tan(r) + 1 / Math.cos(r)) / Math.PI) / 2) * (1 << z);
}

// ── Manager ──

export class TileTextureManager {
  readonly canvas: HTMLCanvasElement;
  private ctx: CanvasRenderingContext2D;
  private cache = new Map<string, HTMLImageElement>();
  private loading = new Set<string>();
  private drawn = new Set<string>();
  private updatePending = false;
  private onUpdate: () => void;
  private vpDebounce: ReturnType<typeof setTimeout> | undefined;

  constructor(
    public readonly width = 4096,
    public readonly height = 2048,
    onUpdate: () => void,
  ) {
    this.canvas = document.createElement('canvas');
    this.canvas.width = width;
    this.canvas.height = height;
    this.ctx = this.canvas.getContext('2d')!;
    this.onUpdate = onUpdate;

    // Ocean background
    this.ctx.fillStyle = '#070b15';
    this.ctx.fillRect(0, 0, width, height);
    this.drawFallbackTexture();
  }

  /** Load base layers progressively (zoom 1 → 3) */
  async init(): Promise<void> {
    await this.loadZoom(1);   // 4 tiles, instant
    await this.loadZoom(2);   // 16 tiles, fast
    this.loadZoom(3);         // 64 tiles, background
  }

  /** Call on every controls-change; internally debounced. */
  loadViewport(lat: number, lng: number, altitude: number): void {
    if (this.vpDebounce) clearTimeout(this.vpDebounce);
    this.vpDebounce = setTimeout(() => this.doLoadViewport(lat, lng, altitude), 250);
  }

  destroy(): void {
    if (this.vpDebounce) clearTimeout(this.vpDebounce);
    this.cache.clear();
    this.loading.clear();
    this.drawn.clear();
  }

  // ── Internal ──

  private doLoadViewport(lat: number, lng: number, altitude: number): void {
    let z: number;
    if (altitude > 1.5) return;        // base tiles sufficient
    else if (altitude > 0.8) z = 4;
    else if (altitude > 0.4) z = 5;
    else z = 6;

    // Estimate visible range in degrees
    const latR = Math.min(85, altitude * 40);
    const lngR = Math.min(180, altitude * 50);

    const n = 1 << z;
    const xMin = Math.max(0, Math.floor(((lng - lngR + 180) / 360) * n));
    const xMax = Math.min(n - 1, Math.ceil(((lng + lngR + 180) / 360) * n));
    const yMin = Math.max(0, Math.floor(latToTileY(Math.min(85, lat + latR), z)));
    const yMax = Math.min(n - 1, Math.ceil(latToTileY(Math.max(-85, lat - latR), z)));

    for (let x = xMin; x <= xMax; x++) {
      for (let y = yMin; y <= yMax; y++) {
        this.loadTile(z, x, y);
      }
    }
  }

  private async loadZoom(z: number): Promise<void> {
    const n = 1 << z;
    const batch: Promise<void>[] = [];
    for (let x = 0; x < n; x++)
      for (let y = 0; y < n; y++)
        batch.push(this.loadTile(z, x, y));
    await Promise.all(batch);
  }

  private drawFallbackTexture(): void {
    const img = new Image();
    img.onload = () => {
      if (this.drawn.size > 0) return;
      this.ctx.drawImage(img, 0, 0, this.width, this.height);
      this.scheduleUpdate();
    };
    img.src = FALLBACK_TEXTURE;
  }

  private loadTile(z: number, x: number, y: number): Promise<void> {
    const k = `${z}/${x}/${y}`;
    if (this.cache.has(k) || this.loading.has(k)) return Promise.resolve();

    this.loading.add(k);
    return new Promise<void>((resolve) => {
      const img = new Image();
      // No crossOrigin needed — tiles served from our own server
      img.onload = () => {
        this.cache.set(k, img);
        this.loading.delete(k);
        this.drawTile(z, x, y, img);
        resolve();
      };
      img.onerror = () => {
        this.loading.delete(k);
        resolve();
      };
      img.src = `${TILE_CDN}/${k}`;
    });
  }

  /**
   * Paint a Mercator tile onto the equirectangular canvas.
   * Slices into horizontal strips for correct reprojection.
   */
  private drawTile(z: number, x: number, y: number, img: HTMLImageElement): void {
    const k = `${z}/${x}/${y}`;
    if (this.drawn.has(k)) return;
    this.drawn.add(k);

    const { width: W, height: H, ctx } = this;

    // Horizontal (longitude) — linear, same in both projections
    const lngL = tileToLng(x, z);
    const lngR = tileToLng(x + 1, z);
    const cx = ((lngL + 180) / 360) * W;
    const cw = ((lngR - lngL) / 360) * W;

    // Vertical — slice into strips for Mercator → equirectangular warp
    const STRIPS = 8;
    const th = img.height;

    for (let s = 0; s < STRIPS; s++) {
      const t0 = s / STRIPS;
      const t1 = (s + 1) / STRIPS;

      // Source rectangle in tile image
      const sy = t0 * th;
      const sh = (t1 - t0) * th;

      // Latitude at strip edges (via Mercator inverse)
      const lat0 = tileToLat(y + t0, z);
      const lat1 = tileToLat(y + t1, z);

      // Destination on equirectangular canvas
      const dy = ((90 - lat0) / 180) * H;
      const dh = ((90 - lat1) / 180) * H - dy;

      ctx.drawImage(img, 0, sy, img.width, sh, cx, dy, cw, Math.max(1, dh));
    }

    this.scheduleUpdate();
  }

  private scheduleUpdate(): void {
    if (this.updatePending) return;
    this.updatePending = true;
    requestAnimationFrame(() => {
      this.updatePending = false;
      this.onUpdate();
    });
  }
}
