import * as THREE from 'three';

// CartoDB Dark Matter — high-performance tactical dark tiles
const IMG = 'https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png';
// OpenStreetMap Roads (monochrome overlay style)
const RDS = 'https://a.basemaps.cartocdn.com/dark_only_labels/{z}/{x}/{y}.png';

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

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

/**
 * Simple tile texture for the 3D globe.
 * Uses ESRI satellite imagery with road overlay.
 * Deep zoom detail is handled by switching to a 2D Leaflet map.
 */
export class TileTextureManager {
  private canvas: HTMLCanvasElement;
  private ctx: CanvasRenderingContext2D;
  private loaded = new Set<string>();
  private loading = new Set<string>();
  private texture: THREE.CanvasTexture | null = null;
  private disposed = false;
  private dirty = false;
  private rafId = 0;
  private W: number;
  private H: number;
  private lastArea = { lat: 0, lon: 0, z: 0 };

  constructor() {
    // 4k is the "sweet spot" for globe textures — 8k often hangs the main thread on GPU upload
    this.W = 4096;
    this.H = 2048;
    this.canvas = document.createElement('canvas');
    this.canvas.width = this.W;
    this.canvas.height = this.H;
    const ctx = this.canvas.getContext('2d', { alpha: false });
    if (!ctx) throw new Error('Canvas 2D unavailable');
    this.ctx = ctx;
    this.ctx.fillStyle = '#050a15';
    this.ctx.fillRect(0, 0, this.W, this.H);
  }

  createTexture(): THREE.CanvasTexture {
    this.texture = new THREE.CanvasTexture(this.canvas);
    this.texture.colorSpace = THREE.SRGBColorSpace;
    this.texture.minFilter = THREE.LinearMipmapLinearFilter;
    this.texture.magFilter = THREE.LinearFilter;
    this.texture.anisotropy = 4;
    this.startDirtyLoop();
    return this.texture;
  }

  async loadBaseLayer(): Promise<void> {
    const z = 3;
    const n = 1 << z;
    const b: Promise<void>[] = [];
    for (let x = 0; x < n; x++)
      for (let y = 0; y < n; y++) b.push(this.fetch(x, y, z, IMG));
    await Promise.allSettled(b);
    this.dirty = true;
  }

  async updateForView(alt: number, lat: number, lon: number): Promise<void> {
    let tz: number;
    if (alt > 2.5) tz = 3;
    else if (alt > 1.5) tz = 4;
    else if (alt > 0.8) tz = 5;
    else if (alt > 0.4) tz = 6;
    else if (alt > 0.18) tz = 7;
    else if (alt > 0.08) tz = 8;
    else tz = 9;

    const p = this.lastArea;
    if (tz === p.z && Math.abs(lat - p.lat) < 2 && Math.abs(lon - p.lon) < 2)
      return;
    this.lastArea = { lat, lon, z: tz };
    if (tz <= 3) return;

    const r = Math.max(alt * 50, 15);
    await this.loadArea(lat, lon, r, tz, IMG);
    if (tz >= 5) await this.loadArea(lat, lon, r, Math.min(tz, 8), RDS);
  }

  private async loadArea(
    cLat: number,
    cLon: number,
    r: number,
    z: number,
    url: string,
  ): Promise<void> {
    const n = 1 << z;
    const minLat = Math.max(-85, cLat - r);
    const maxLat = Math.min(85, cLat + r);
    const yMin = Math.max(0, latToTileY(maxLat, z));
    const yMax = Math.min(n - 1, latToTileY(minLat, z));
    const xMin = Math.floor(((cLon - r + 180) / 360) * n);
    const xMax = Math.floor(((cLon + r + 180) / 360) * n);
    const tiles: { x: number, y: number, dist: number }[] = [];
    
    // Calculate center tile coords to sort by distance
    const cx = ((Math.floor(((cLon + 180) / 360) * n) % n) + n) % n;
    const cy = latToTileY(cLat, z);

    for (let tx = xMin; tx <= xMax; tx++) {
      const x = ((tx % n) + n) % n;
      for (let y = yMin; y <= yMax; y++) {
        const pre = url === IMG ? 'I' : 'R';
        const key = `${pre}${z}/${x}/${y}`;
        if (!this.loaded.has(key) && !this.loading.has(key)) {
          // Manhattan distance for sorting
          const dist = Math.abs(x - cx) + Math.abs(y - cy);
          tiles.push({ x, y, dist });
        }
      }
    }

    // Sort tiles by distance from center
    tiles.sort((a, b) => a.dist - b.dist);

    // Process in batches of 6 (slightly smaller for better prioritization)
    for (let i = 0; i < tiles.length; i += 6) {
      const batch = tiles.slice(i, i + 6).map(({ x, y }) => this.fetch(x, y, z, url));
      await Promise.allSettled(batch);
      this.dirty = true;
    }
  }

  private async fetch(x: number, y: number, z: number, base: string): Promise<void> {
    const pre = base === IMG ? 'I' : 'R';
    const key = `${pre}${z}/${x}/${y}`;
    if (this.loaded.has(key) || this.loading.has(key)) return Promise.resolve();
    this.loading.add(key);

    const url = base
      .replace('{z}', String(z))
      .replace('{y}', String(y))
      .replace('{x}', String(x));

    try {
      const cache = await caches.open('tile-cache-v1');
      let response = await cache.match(url);

      if (!response) {
        response = await fetch(url);
        if (response.ok) {
          await cache.put(url, response.clone());
        }
      }

      if (!response.ok) throw new Error(`${response.status}`);
      
      const blob = await response.blob();
      const bmp = await createImageBitmap(blob);

      if (!this.disposed) {
        this.drawBitmap(bmp, x, y, z);
        this.loaded.add(key);
      }
      bmp.close();
      this.loading.delete(key);
    } catch (err) {
      this.loading.delete(key);
      // Fallback: if cache fails, try fetching directly
      if (!this.disposed) {
         // Silently fail or retry without cache
      }
    }
  }

  private drawBitmap(bmp: ImageBitmap, x: number, y: number, z: number): void {
    const n = 1 << z;
    const lonL = (x / n) * 360 - 180;
    const lonR = ((x + 1) / n) * 360 - 180;
    const latT = tileYToLat(y, z);
    const latB = tileYToLat(y + 1, z);
    const px0 = Math.round(((lonL + 180) / 360) * this.W);
    const px1 = Math.round(((lonR + 180) / 360) * this.W);
    const py0 = Math.round(((90 - latT) / 180) * this.H);
    const py1 = Math.round(((90 - latB) / 180) * this.H);
    this.ctx.drawImage(bmp, px0, py0, px1 - px0 + 1, py1 - py0 + 1);
  }

  private startDirtyLoop(): void {
    let lastCheck = 0;
    const tick = (now: number) => {
      if (this.disposed) return;
      // Throttle texture uploads to 5fps max (every 200ms) — uploading an 8k texture is expensive
      if (this.dirty && this.texture && now - lastCheck > 200) {
        this.texture.needsUpdate = true;
        this.dirty = false;
        lastCheck = now;
      }
      this.rafId = requestAnimationFrame(tick);
    };
    this.rafId = requestAnimationFrame(tick);
  }

  dispose(): void {
    this.disposed = true;
    cancelAnimationFrame(this.rafId);
    this.loaded.clear();
    this.loading.clear();
    if (this.texture) { this.texture.dispose(); this.texture = null; }
  }
}
