/* 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/. */

/**
 * Point cloud rendering preferences.
 *
 * The renderer reads these via `usePointCloudSync`; UI components write
 * them via the actions below. EDL is opt-in (default on) — costs ~5
 * extra texture taps per pixel.
 */

import type { StateCreator } from 'zustand';

export type PointColorModeUi = 'rgb' | 'classification' | 'intensity' | 'height' | 'fixed' | 'deviation';
export type PointSizeModeUi = 'fixed-px' | 'adaptive-world' | 'attenuated';

export interface PointCloudSlice {
  pointCloudColorMode: PointColorModeUi;
  pointCloudFixedColor: [number, number, number, number];
  /** Splat sizing strategy. Default: 'fixed-px' (sized by the px slider). */
  pointCloudSizeMode: PointSizeModeUi;
  /** Splat size in pixels (fixed/attenuated) or upper cap (attenuated). 1..20. */
  pointCloudPointSize: number;
  /** World-space splat radius in metres for adaptive/attenuated modes.
   *  Typical scans: 0.005–0.05. Default 0.02. */
  pointCloudWorldRadius: number;
  /** Render splats as discs vs squares. Default true. */
  pointCloudRoundShape: boolean;
  /** Enable Eye-Dome Lighting post-pass. Default true. */
  pointCloudEdlEnabled: boolean;
  /** EDL strength multiplier. 0..3, default 1. */
  pointCloudEdlStrength: number;
  /**
   * Per-ASPRS-class visibility bitmask (32 bits = covers classes
   * 0..31, the LAS 1.4 standard range). Bit `i` set → class `i`
   * visible. Default `0xFFFFFFFF` (all visible). Only point clouds
   * carry classifications; meshes ignore this.
   */
  pointCloudClassMask: number;
  /**
   * Stride-cull factor for the splat shader. 1 = render every point,
   * N>1 = render every Nth point. Used by the section-plane slider's
   * drag-preview path so dragging over a 100M-point scan stays
   * responsive. Defaults to 1 (full density).
   */
  pointCloudPreviewStride: number;
  /**
   * BIM↔scan deviation heatmap range. `centerOffset` shifts the
   * "white" point off zero (handy when a scan has a global offset
   * from the model); `halfRange` is the metres mapped to ±1 on the
   * blue-white-red ramp. Defaults to (0, 0.05) — ±5cm.
   */
  pointCloudDeviationCenterOffset: number;
  pointCloudDeviationHalfRange: number;
  /**
   * True once `Renderer.computeDeviations` has populated the deviation
   * buffers for the current point cloud + mesh set. UI gates the
   * "Deviation" colour-mode option on this flag so users don't get a
   * confusing all-blue rendering when nothing has been computed.
   */
  pointCloudDeviationComputed: boolean;
  /**
   * Best-effort count of point cloud assets currently uploaded to the
   * renderer. Updated by ingest paths; UI uses it to show/hide the
   * controls panel and the EDL post-pass.
   */
  pointCloudAssetCount: number;
  setPointCloudColorMode: (mode: PointColorModeUi) => void;
  setPointCloudFixedColor: (rgba: [number, number, number, number]) => void;
  setPointCloudSizeMode: (mode: PointSizeModeUi) => void;
  setPointCloudPointSize: (px: number) => void;
  setPointCloudWorldRadius: (m: number) => void;
  setPointCloudRoundShape: (enabled: boolean) => void;
  setPointCloudEdlEnabled: (enabled: boolean) => void;
  setPointCloudEdlStrength: (strength: number) => void;
  setPointCloudClassMask: (mask: number) => void;
  /** Toggle a single ASPRS class. `classId` is clamped to 0..31. */
  togglePointCloudClass: (classId: number) => void;
  /** Set the stride-cull factor (1 = full density). */
  setPointCloudPreviewStride: (stride: number) => void;
  setPointCloudDeviationCenterOffset: (m: number) => void;
  setPointCloudDeviationHalfRange: (m: number) => void;
  setPointCloudDeviationComputed: (computed: boolean) => void;
  setPointCloudAssetCount: (count: number) => void;
  incrementPointCloudAssetCount: (n?: number) => void;
}

/**
 * Single source of truth for the slice's runtime field defaults.
 * Both the slice initializer and `resetViewerState` consume this so
 * the two paths can't drift.
 */
export const POINT_CLOUD_DEFAULTS = {
  // Fixed-px is the default so the size slider feels responsive on first
  // contact. `attenuated` is nicer at extreme zooms but its "slider =
  // upper cap" semantic confuses users at typical wide views because the
  // projected world radius sits well below the cap.
  pointCloudColorMode: 'rgb' as PointColorModeUi,
  pointCloudFixedColor: [1, 1, 1, 1] as [number, number, number, number],
  pointCloudSizeMode: 'fixed-px' as PointSizeModeUi,
  pointCloudPointSize: 4,
  pointCloudWorldRadius: 0.02,
  pointCloudRoundShape: true,
  pointCloudEdlEnabled: true,
  pointCloudEdlStrength: 1,
  // 0xFFFFFFFF — all 32 classes visible. Stored as `-1 >>> 0` to
  // keep the value as an unsigned 32-bit integer; JS doesn't have
  // a u32 literal type so we round-trip through `>>> 0`.
  pointCloudClassMask: 0xFFFFFFFF,
  pointCloudPreviewStride: 1,
  pointCloudDeviationCenterOffset: 0,
  pointCloudDeviationHalfRange: 0.05,
  pointCloudDeviationComputed: false,
  pointCloudAssetCount: 0,
} as const;

export const createPointCloudSlice: StateCreator<PointCloudSlice, [], [], PointCloudSlice> = (set) => ({
  ...POINT_CLOUD_DEFAULTS,
  // Re-spread typed-array fields so consumers get fresh references
  // instead of the readonly literal in POINT_CLOUD_DEFAULTS.
  pointCloudFixedColor: [...POINT_CLOUD_DEFAULTS.pointCloudFixedColor] as [number, number, number, number],
  setPointCloudColorMode: (mode) => set({ pointCloudColorMode: mode }),
  setPointCloudFixedColor: (rgba) => set({ pointCloudFixedColor: rgba }),
  setPointCloudSizeMode: (mode) => set({ pointCloudSizeMode: mode }),
  // NaN/Infinity slip past Math.max+min unchanged ((NaN < x) === false),
  // so guard with isFinite to keep invalid values out of GPU uniforms.
  setPointCloudPointSize: (px) => set({
    pointCloudPointSize: Number.isFinite(px) ? Math.max(1, Math.min(20, px)) : 4,
  }),
  setPointCloudWorldRadius: (m) => set({
    pointCloudWorldRadius: Number.isFinite(m) ? Math.max(1e-4, m) : 0.02,
  }),
  setPointCloudRoundShape: (enabled) => set({ pointCloudRoundShape: enabled }),
  setPointCloudEdlEnabled: (enabled) => set({ pointCloudEdlEnabled: enabled }),
  setPointCloudEdlStrength: (strength) => set({
    pointCloudEdlStrength: Number.isFinite(strength) ? Math.max(0, Math.min(3, strength)) : 1,
  }),
  setPointCloudClassMask: (mask) => set({
    // Coerce through `>>> 0` to keep the stored value as an unsigned
    // 32-bit integer; non-finite / negative inputs reset to "all on".
    pointCloudClassMask: Number.isFinite(mask) ? (mask >>> 0) : 0xFFFFFFFF,
  }),
  togglePointCloudClass: (classId) => set((s) => {
    const c = Math.max(0, Math.min(31, classId | 0));
    const bit = 1 << c;
    // XOR flips the bit; coerce through `>>> 0` so the stored value
    // stays in the unsigned 32-bit range.
    return { pointCloudClassMask: (s.pointCloudClassMask ^ bit) >>> 0 };
  }),
  setPointCloudPreviewStride: (stride) => set({
    pointCloudPreviewStride: Number.isFinite(stride)
      ? Math.max(1, Math.min(256, Math.floor(stride) || 1))
      : 1,
  }),
  setPointCloudDeviationCenterOffset: (m) => set({
    pointCloudDeviationCenterOffset: Number.isFinite(m) ? m : 0,
  }),
  setPointCloudDeviationHalfRange: (m) => set({
    // halfRange must stay strictly positive — a zero or negative value
    // would NaN the GPU ramp's division. Clamp to 0.1 mm minimum.
    pointCloudDeviationHalfRange: Number.isFinite(m) ? Math.max(1e-4, m) : 0.05,
  }),
  setPointCloudDeviationComputed: (computed) => set({ pointCloudDeviationComputed: computed }),
  setPointCloudAssetCount: (count) => set({
    pointCloudAssetCount: Number.isFinite(count) ? Math.max(0, count) : 0,
  }),
  incrementPointCloudAssetCount: (n = 1) => set((s) => ({
    pointCloudAssetCount: Number.isFinite(n)
      ? Math.max(0, s.pointCloudAssetCount + n)
      : s.pointCloudAssetCount,
  })),
});
