/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

/**
 * IFC-Lite bridge - initializes and manages IFC-Lite WASM for geometry processing
 * Replaces web-ifc-bridge.ts with native IFC-Lite implementation (1.9x faster)
 */

import { createLogger } from '@ifc-lite/data';
import type { TessellationQuality } from './types.js';
import init, {
  IfcAPI,
  SymbolicRepresentationCollection,
  SymbolicPolyline,
  SymbolicCircle,
  ProfileCollection,
  ProfileEntryJs,
  GridAxisCollection,
  GridAxisJs,
} from '@ifc-lite/wasm';
export type {
  SymbolicRepresentationCollection,
  SymbolicPolyline,
  SymbolicCircle,
  ProfileCollection,
  ProfileEntryJs,
  GridAxisCollection,
  GridAxisJs,
};

const log = createLogger('Geometry');
const FATAL_WASM_RELOAD_REQUIRED_MESSAGE = 'IFC-Lite WASM cannot recover from a fatal runtime error within the same document lifetime. Reload the page or recreate the worker process before calling init() again.';
let fatalWasmRuntimeError: Error | null = null;

/**
 * Typed wrapper for the IFC-Lite WASM API including the optional
 * `setMergeLayers` extension. The base `IfcAPI` declaration in
 * `packages/wasm/pkg/ifc-lite.d.ts` is regenerated by the Rust build;
 * after the Rust agent lands the merge-layers contract this cast
 * becomes redundant, but until then it lets us call the method without
 * relying on `as any` or `@ts-ignore`.
 */
type IfcAPIWithMerge = IfcAPI & {
  setMergeLayers?: (enabled: boolean) => void;
  setComputeGeometryHashes?: (tolerance?: number | null) => void;
  setTessellationQuality?: (level?: string | null) => void;
};

export class IfcLiteBridge {
  private ifcApi: IfcAPI | null = null;
  private initialized: boolean = false;
  /**
   * When true, the WASM mesh emitters drop `IfcBuildingElementPart`
   * meshes whose parent wall is sliceable (Revit-style multilayer
   * walls). Layer colour info is preserved on the parent solid. The
   * flag is forwarded to the underlying IfcAPI on `init` and on every
   * `setMergeLayers` call; consumers can still read it back via the
   * getter for telemetry / UI badges.
   */
  private mergeLayers: boolean = false;
  /**
   * Per-entity geometry-hash tolerance in metres, or `null` when
   * fingerprinting is off (the default — zero overhead). When set, the
   * WASM mesh pass emits an RTC-invariant `geometryHashValues` array used
   * by the model-diff / compare feature (issue #924). Cached here and
   * forwarded to the IfcAPI on `init` + every `setComputeGeometryHashes`
   * call, mirroring the `mergeLayers` replay pattern.
   */
  private geometryHashTolerance: number | null = null;
  /**
   * Tessellation detail level (issue #976), or `null` for the engine
   * default (`'medium'`, byte-for-byte identical to the pre-quality
   * pipeline). Cached here and forwarded to the IfcAPI on `init` + every
   * `setTessellationQuality` call, mirroring the `mergeLayers` replay
   * pattern.
   */
  private tessellationQuality: TessellationQuality | null = null;

  private isWasmRuntimeError(error: unknown): boolean {
    return error instanceof WebAssembly.RuntimeError;
  }

  private markFatalWasmRuntimeError(): void {
    fatalWasmRuntimeError = new Error(FATAL_WASM_RELOAD_REQUIRED_MESSAGE);
    this.reset();
  }

  /**
   * Initialize IFC-Lite WASM
   * The WASM binary is automatically resolved from the same location as the JS module
   */
  async init(): Promise<void> {
    if (this.initialized) return;
    if (fatalWasmRuntimeError) {
      throw fatalWasmRuntimeError;
    }

    try {
      // Initialize WASM module - wasm-bindgen automatically resolves the WASM URL
      // from import.meta.url, no need to manually construct paths
      await init();

      // The WASM bundle has no in-WASM thread pool; rayon `par_iter()`
      // (e.g. FacetedBrep preprocessing) runs sequentially on the main
      // thread. Cross-worker parallelism is provided at a higher level by
      // the N-worker pool in `geometry-parallel.ts`.
      log.debug('Geometry processing: single-threaded WASM (N-worker pool handles parallelism)');

      this.ifcApi = new IfcAPI();
      // Re-apply the cached merge-layers flag so callers can call
      // `setMergeLayers(true)` BEFORE `init()` and still get the
      // expected behaviour on the freshly-constructed IfcAPI.
      this.applyMergeLayers();
      // Same replay contract for the geometry-hash toggle.
      this.applyComputeGeometryHashes();
      // …and for the tessellation-quality level (issue #976).
      this.applyTessellationQuality();
      this.initialized = true;
      log.info('WASM geometry engine initialized');
    } catch (error) {
      log.error('Failed to initialize WASM geometry engine', error, {
        operation: 'init',
      });
      if (this.isWasmRuntimeError(error)) {
        this.markFatalWasmRuntimeError();
      } else {
        this.reset();
      }
      throw error;
    }
  }

  /**
   * Reset the JS wrapper state.
   * This does not reload wasm-bindgen's module singleton after a fatal WASM panic.
   */
  reset(): void {
    this.initialized = false;
    this.ifcApi = null;
  }

  /**
   * Parse IFC content and return symbolic representations (Plan, Annotation, FootPrint)
   * These are pre-authored 2D curves for architectural drawings
   */
  parseSymbolicRepresentations(content: string): SymbolicRepresentationCollection {
    if (!this.ifcApi) {
      throw new Error('IFC-Lite not initialized. Call init() first.');
    }
    try {
      const collection = this.ifcApi.parseSymbolicRepresentations(content);
      log.debug(`Parsed ${collection.totalCount} symbolic items (${collection.polylineCount} polylines, ${collection.circleCount} circles)`, { operation: 'parseSymbolicRepresentations' });
      return collection;
    } catch (error) {
      log.error('Failed to parse symbolic representations', error, {
        operation: 'parseSymbolicRepresentations',
        data: { contentLength: content.length },
      });
      if (this.isWasmRuntimeError(error)) {
        this.markFatalWasmRuntimeError();
      }
      throw error;
    }
  }

  /**
   * Parse IfcAlignment directrix curves into a flat Float32Array of 3D
   * line-list vertices `[x0,y0,z0, x1,y1,z1, …]` in renderer Y-up world space
   * (RTC-subtracted, metres). Feed straight to `renderer.uploadAlignmentLines3D`.
   * Empty when the file has no alignments.
   */
  parseAlignmentLines(content: string): Float32Array {
    if (!this.ifcApi) {
      throw new Error('IFC-Lite not initialized. Call init() first.');
    }
    try {
      const vertices = this.ifcApi.parseAlignmentLines(content);
      log.debug(`Parsed ${vertices.length / 3} alignment line vertices`, { operation: 'parseAlignmentLines' });
      return vertices;
    } catch (error) {
      log.error('Failed to parse alignment lines', error, {
        operation: 'parseAlignmentLines',
        data: { contentLength: content.length },
      });
      if (this.isWasmRuntimeError(error)) {
        this.markFatalWasmRuntimeError();
      }
      throw error;
    }
  }

  /**
   * Parse `IfcGrid` / `IfcGridAxis` into a flat Float32Array of 3D line-list
   * vertices `[x0,y0,z0, x1,y1,z1, …]` (one segment per axis) in renderer Y-up
   * world space (RTC-subtracted, metres) — the same frame the streamed meshes
   * render in, so grids overlay the model by construction (issue #945). Feed
   * straight to a line pipeline (e.g. `renderer.uploadAnnotationLines3D`).
   * Empty when the file has no grids.
   */
  parseGridLines(content: string): Float32Array {
    if (!this.ifcApi) {
      throw new Error('IFC-Lite not initialized. Call init() first.');
    }
    try {
      const vertices = this.ifcApi.parseGridLines(content);
      log.debug(`Parsed ${vertices.length / 3} grid line vertices`, { operation: 'parseGridLines' });
      return vertices;
    } catch (error) {
      log.error('Failed to parse grid lines', error, {
        operation: 'parseGridLines',
        data: { contentLength: content.length },
      });
      if (this.isWasmRuntimeError(error)) {
        this.markFatalWasmRuntimeError();
      }
      throw error;
    }
  }

  /**
   * Parse `IfcGrid` / `IfcGridAxis` into structured per-axis data
   * (`{ gridId, axisId, tag, start, end }`) in renderer Y-up world space
   * (RTC-subtracted, metres). Use this when you also need the axis tags to
   * render grid bubbles / labels. See issue #945.
   */
  parseGridAxes(content: string): GridAxisCollection {
    if (!this.ifcApi) {
      throw new Error('IFC-Lite not initialized. Call init() first.');
    }
    try {
      const collection = this.ifcApi.parseGridAxes(content);
      log.debug(`Parsed ${collection.length} grid axes`, { operation: 'parseGridAxes' });
      return collection;
    } catch (error) {
      log.error('Failed to parse grid axes', error, {
        operation: 'parseGridAxes',
        data: { contentLength: content.length },
      });
      if (this.isWasmRuntimeError(error)) {
        this.markFatalWasmRuntimeError();
      }
      throw error;
    }
  }

  /**
   * Extract raw profile polygons from all IfcExtrudedAreaSolid building elements.
   *
   * Returns profile outlines + placement transforms for clean 2D projection
   * without the tessellation artifacts that EdgeExtractor produces.
   *
   * @param content    Raw IFC file text.
   * @param modelIndex Federation model index (use 0 for single-model files).
   */
  extractProfiles(content: string, modelIndex: number = 0): ProfileCollection {
    if (!this.ifcApi) {
      throw new Error('IFC-Lite not initialized. Call init() first.');
    }
    try {
      const collection = this.ifcApi.extractProfiles(content, modelIndex);
      log.debug(`Extracted ${collection.length} profiles`, { operation: 'extractProfiles' });
      return collection;
    } catch (error) {
      log.error('Failed to extract profiles', error, {
        operation: 'extractProfiles',
        data: { contentLength: content.length },
      });
      if (this.isWasmRuntimeError(error)) {
        this.markFatalWasmRuntimeError();
      }
      throw error;
    }
  }

  /**
   * Get IFC-Lite API instance
   */
  getApi(): IfcAPI {
    if (!this.ifcApi) {
      throw new Error('IFC-Lite not initialized. Call init() first.');
    }
    return this.ifcApi;
  }

  /**
   * Check if initialized
   */
  isInitialized(): boolean {
    return this.initialized;
  }

  /**
   * Get version
   */
  getVersion(): string {
    return this.ifcApi?.version ?? 'unknown';
  }

  /**
   * Enable / disable Revit-style multilayer-wall merging (issue #540).
   * Safe to call before `init()` — the value is cached and re-applied
   * on the freshly-constructed IfcAPI.
   */
  setMergeLayers(enabled: boolean): void {
    this.mergeLayers = enabled;
    if (!this.ifcApi) return; // init() will apply on the new IfcAPI
    this.applyMergeLayers();
  }

  /** Read back the merge-layers flag for UI surfaces. */
  getMergeLayers(): boolean {
    return this.mergeLayers;
  }

  /**
   * Forward the cached `mergeLayers` flag to the underlying IfcAPI.
   * Until the Rust agent regenerates the WASM `.d.ts` to add this
   * method, we route through a narrow typed wrapper so the codebase
   * doesn't need `as any` / `@ts-ignore`. If the method is missing
   * (older WASM build), we log and continue — the safe default.
   */
  private applyMergeLayers(): void {
    if (!this.ifcApi) return;
    const api = this.ifcApi as IfcAPIWithMerge;
    if (typeof api.setMergeLayers !== 'function') {
      log.warn('setMergeLayers not present on WASM API — flag tracked but suppression is a no-op until WASM is rebuilt', {
        operation: 'setMergeLayers',
        data: { requested: this.mergeLayers },
      });
      return;
    }
    api.setMergeLayers(this.mergeLayers);
    log.debug(`mergeLayers=${this.mergeLayers}`, { operation: 'setMergeLayers' });
  }

  /**
   * Enable / disable per-entity geometry fingerprinting (issue #924).
   * Pass a positive tolerance in metres to enable; `null` disables.
   * Safe to call before `init()` — the value is cached and re-applied on
   * the freshly-constructed IfcAPI.
   */
  setComputeGeometryHashes(tolerance: number | null): void {
    this.geometryHashTolerance = tolerance != null && tolerance > 0 ? tolerance : null;
    if (!this.ifcApi) return; // init() will apply on the new IfcAPI
    this.applyComputeGeometryHashes();
  }

  /** Read back the active geometry-hash tolerance (null = disabled). */
  getComputeGeometryHashes(): number | null {
    return this.geometryHashTolerance;
  }

  /**
   * Forward the cached geometry-hash tolerance to the underlying IfcAPI.
   * Guards against an older WASM build that predates the binding (the
   * artifact is rebuilt in CI) so the flag degrades to a no-op rather
   * than throwing — same defensive shape as `applyMergeLayers`.
   */
  private applyComputeGeometryHashes(): void {
    if (!this.ifcApi) return;
    const api = this.ifcApi as IfcAPIWithMerge;
    if (typeof api.setComputeGeometryHashes !== 'function') {
      if (this.geometryHashTolerance != null) {
        log.warn('setComputeGeometryHashes not present on WASM API — geometry diff hashes unavailable until WASM is rebuilt', {
          operation: 'setComputeGeometryHashes',
          data: { requested: this.geometryHashTolerance },
        });
      }
      return;
    }
    api.setComputeGeometryHashes(this.geometryHashTolerance);
    log.debug(`computeGeometryHashes=${this.geometryHashTolerance ?? 'off'}`, {
      operation: 'setComputeGeometryHashes',
    });
  }

  /**
   * Select the tessellation detail level for curved geometry (issue #976).
   * `null` restores the engine default (`'medium'` — output identical to the
   * pre-quality pipeline). Safe to call before `init()` — the value is cached
   * and re-applied on the freshly-constructed IfcAPI. Applies to meshes
   * produced AFTER the call; already-emitted geometry is not regenerated.
   */
  setTessellationQuality(level: TessellationQuality | null): void {
    this.tessellationQuality = level;
    if (!this.ifcApi) return; // init() will apply on the new IfcAPI
    this.applyTessellationQuality();
  }

  /** Read back the active tessellation quality (null = engine default). */
  getTessellationQuality(): TessellationQuality | null {
    return this.tessellationQuality;
  }

  /**
   * Forward the cached tessellation quality to the underlying IfcAPI.
   * Guards against an older WASM build that predates the binding so the
   * level degrades to a no-op (default density) rather than throwing —
   * same defensive shape as `applyMergeLayers`.
   */
  private applyTessellationQuality(): void {
    if (!this.ifcApi) return;
    const api = this.ifcApi as IfcAPIWithMerge;
    if (typeof api.setTessellationQuality !== 'function') {
      if (this.tessellationQuality != null) {
        log.warn('setTessellationQuality not present on WASM API — level ignored until WASM is rebuilt', {
          operation: 'setTessellationQuality',
          data: { requested: this.tessellationQuality },
        });
      }
      return;
    }
    api.setTessellationQuality(this.tessellationQuality);
    log.debug(`tessellationQuality=${this.tessellationQuality ?? 'default'}`, {
      operation: 'setTessellationQuality',
    });
  }
}
