/// A snapshot of a scene's blendable look (image-based lighting, exposure,
/// tone mapping, and post-processing), captured as a copyable value that can be
/// interpolated. Drives scripted environment transitions.
library;

import 'package:vector_math/vector_math.dart';

import 'package:flutter_scene/src/ambient_occlusion.dart';
import 'package:flutter_scene/src/material/environment.dart';
import 'package:flutter_scene/src/scene.dart';
import 'package:flutter_scene/src/sky_environment.dart';
import 'package:flutter_scene/src/skybox.dart';
import 'package:flutter_scene/src/sun_light.dart';
import 'package:flutter_scene/src/tone_mapping.dart';

double _lerp(double a, double b, double t) => a + (b - a) * t;

Vector3 _lerpVec3(Vector3 a, Vector3 b, double t) =>
    Vector3(_lerp(a.x, b.x, t), _lerp(a.y, b.y, t), _lerp(a.z, b.z, t));

/// A copyable, interpolatable snapshot of a [Scene]'s scene-wide look.
///
/// Read one with `Scene.environmentSettings` and apply one by assigning it
/// back. [lerp] blends two snapshots: continuous fields (exposure, intensities,
/// color-grading and post-effect parameters) interpolate; discrete fields (the
/// image-based-lighting environment, skybox, sky lighting, sun light, tone
/// mapping operator, and each effect's `enabled` flag) switch at the halfway
/// point so the endpoints reproduce the inputs exactly. To cross-fade an effect
/// on or off smoothly, animate its amount (e.g. bloom intensity) rather than
/// relying on the flag.
///
/// A typical scripted transition drives `t` from an animation each frame:
/// ```dart
/// scene.environmentSettings = EnvironmentSettings.lerp(dayLook, nightLook, t);
/// ```
/// {@category Lighting and environment}
class EnvironmentSettings {
  // TODO(environment-settings): compose per-feature snapshots that each
  // capture, apply, and interpolate their fields in one place.
  /// Creates a settings snapshot. Most callers use `Scene.environmentSettings`
  /// or [EnvironmentSettings.lerp] instead of this directly.
  EnvironmentSettings({
    this.environment,
    this.skybox,
    this.skyEnvironment,
    this.sunLight,
    this.toneMapping = ToneMappingMode.pbrNeutral,
    this.agxWhite = 16.29,
    this.agxContrast = 1.25,
    this.environmentIntensity = 1.0,
    this.exposure = 1.0,
    this.colorGradingEnabled = false,
    this.brightness = 1.0,
    this.contrast = 1.0,
    this.saturation = 1.0,
    this.temperature = 0.0,
    this.tint = 0.0,
    Vector3? lift,
    Vector3? gamma,
    Vector3? gain,
    this.bloomEnabled = false,
    this.bloomThreshold = 1.0,
    this.bloomIntensity = 0.5,
    this.bloomScatter = 0.7,
    this.vignetteEnabled = false,
    this.vignetteIntensity = 0.5,
    this.vignetteRadius = 0.75,
    this.vignetteSmoothness = 0.5,
    this.chromaticAberrationEnabled = false,
    this.chromaticAberrationIntensity = 0.5,
    this.filmGrainEnabled = false,
    this.filmGrainIntensity = 0.3,
    this.ambientOcclusionEnabled = false,
    this.ambientOcclusionRadius = 0.33,
    this.ambientOcclusionIntensity = 2.0,
    this.ambientOcclusionBias = 0.07,
    this.ambientOcclusionPower = 1.5,
    this.ambientOcclusionDetail = 0.5,
    this.ambientOcclusionHorizonAngle = 0.06,
    this.ambientOcclusionDirectLightAffect = 0.0,
    this.ambientOcclusionSampleCount = 16,
    this.ambientOcclusionHalfResolution = true,
    this.ambientOcclusionDepthMipChain = false,
    this.ambientOcclusionSpecularMode = SpecularAmbientOcclusionMode.none,
  }) : lift = lift ?? Vector3.zero(),
       gamma = gamma ?? Vector3.all(1.0),
       gain = gain ?? Vector3.all(1.0);

  // Image-based lighting and sky (discrete: switched, not blended).
  EnvironmentMap? environment;
  Skybox? skybox;
  SkyEnvironment? skyEnvironment;
  SunLight? sunLight;
  ToneMappingMode toneMapping;
  double agxWhite;
  double agxContrast;

  // Scene scalars (continuous).
  double environmentIntensity;
  double exposure;

  // Color grading.
  bool colorGradingEnabled;
  double brightness;
  double contrast;
  double saturation;
  double temperature;
  double tint;
  Vector3 lift;
  Vector3 gamma;
  Vector3 gain;

  // Bloom.
  bool bloomEnabled;
  double bloomThreshold;
  double bloomIntensity;
  double bloomScatter;

  // Vignette.
  bool vignetteEnabled;
  double vignetteIntensity;
  double vignetteRadius;
  double vignetteSmoothness;

  // Chromatic aberration.
  bool chromaticAberrationEnabled;
  double chromaticAberrationIntensity;

  // Film grain.
  bool filmGrainEnabled;
  double filmGrainIntensity;

  // Ambient occlusion.
  bool ambientOcclusionEnabled;
  double ambientOcclusionRadius;
  double ambientOcclusionIntensity;
  double ambientOcclusionBias;
  double ambientOcclusionPower;
  double ambientOcclusionDetail;
  double ambientOcclusionHorizonAngle;
  double ambientOcclusionDirectLightAffect;
  int ambientOcclusionSampleCount;
  bool ambientOcclusionHalfResolution;
  bool ambientOcclusionDepthMipChain;
  SpecularAmbientOcclusionMode ambientOcclusionSpecularMode;

  /// Reads the current look of [scene] into a snapshot. The IBL/sky references
  /// are shared (not deep-copied); the scalar look is captured by value.
  factory EnvironmentSettings.fromScene(Scene scene) {
    final cg = scene.postProcess.colorGrading;
    final bloom = scene.postProcess.bloom;
    final vignette = scene.postProcess.vignette;
    final ca = scene.postProcess.chromaticAberration;
    final grain = scene.postProcess.filmGrain;
    final ao = scene.ambientOcclusion;
    return EnvironmentSettings(
      environment: scene.environment,
      skybox: scene.skybox,
      skyEnvironment: scene.skyEnvironment,
      sunLight: scene.sunLight,
      toneMapping: scene.toneMapping,
      agxWhite: scene.agxWhite,
      agxContrast: scene.agxContrast,
      environmentIntensity: scene.environmentIntensity,
      exposure: scene.exposure,
      colorGradingEnabled: cg.enabled,
      brightness: cg.brightness,
      contrast: cg.contrast,
      saturation: cg.saturation,
      temperature: cg.temperature,
      tint: cg.tint,
      lift: cg.lift.clone(),
      gamma: cg.gamma.clone(),
      gain: cg.gain.clone(),
      bloomEnabled: bloom.enabled,
      bloomThreshold: bloom.threshold,
      bloomIntensity: bloom.intensity,
      bloomScatter: bloom.scatter,
      vignetteEnabled: vignette.enabled,
      vignetteIntensity: vignette.intensity,
      vignetteRadius: vignette.radius,
      vignetteSmoothness: vignette.smoothness,
      chromaticAberrationEnabled: ca.enabled,
      chromaticAberrationIntensity: ca.intensity,
      filmGrainEnabled: grain.enabled,
      filmGrainIntensity: grain.intensity,
      ambientOcclusionEnabled: ao.enabled,
      ambientOcclusionRadius: ao.radius,
      ambientOcclusionIntensity: ao.intensity,
      ambientOcclusionBias: ao.bias,
      ambientOcclusionPower: ao.power,
      ambientOcclusionDetail: ao.detail,
      ambientOcclusionHorizonAngle: ao.horizonAngle,
      ambientOcclusionDirectLightAffect: ao.directLightAffect,
      ambientOcclusionSampleCount: ao.sampleCount,
      ambientOcclusionHalfResolution: ao.halfResolution,
      ambientOcclusionDepthMipChain: ao.depthMipChain,
      ambientOcclusionSpecularMode: ao.specularMode,
    );
  }

  /// Applies the environment, sky, exposure, and tone mapping to [scene].
  void applyLookTo(Scene scene) {
    // A sky-lit look has no static [environment]; its sky bakes the scene
    // environment over the next frames. Keep the previously baked environment
    // until then, so re-applying a sky-lit look (an editor edit, or the volume
    // blend each frame) does not briefly drop to the default environment.
    if (environment != null || skyEnvironment == null) {
      scene.environment = environment;
    }
    scene.skybox = skybox;
    scene.skyEnvironment = skyEnvironment;
    scene.sunLight = sunLight;
    scene.toneMapping = toneMapping;
    scene.agxWhite = agxWhite;
    scene.agxContrast = agxContrast;
    scene.environmentIntensity = environmentIntensity;
    scene.exposure = exposure;
  }

  /// Applies this snapshot to [scene], mutating its live look fields.
  void applyTo(Scene scene) {
    applyLookTo(scene);

    final cg = scene.postProcess.colorGrading;
    cg
      ..enabled = colorGradingEnabled
      ..brightness = brightness
      ..contrast = contrast
      ..saturation = saturation
      ..temperature = temperature
      ..tint = tint
      ..lift.setFrom(lift)
      ..gamma.setFrom(gamma)
      ..gain.setFrom(gain);

    final bloom = scene.postProcess.bloom;
    bloom
      ..enabled = bloomEnabled
      ..threshold = bloomThreshold
      ..intensity = bloomIntensity
      ..scatter = bloomScatter;

    final vignette = scene.postProcess.vignette;
    vignette
      ..enabled = vignetteEnabled
      ..intensity = vignetteIntensity
      ..radius = vignetteRadius
      ..smoothness = vignetteSmoothness;

    scene.postProcess.chromaticAberration
      ..enabled = chromaticAberrationEnabled
      ..intensity = chromaticAberrationIntensity;

    scene.postProcess.filmGrain
      ..enabled = filmGrainEnabled
      ..intensity = filmGrainIntensity;

    scene.ambientOcclusion
      ..enabled = ambientOcclusionEnabled
      ..radius = ambientOcclusionRadius
      ..intensity = ambientOcclusionIntensity
      ..bias = ambientOcclusionBias
      ..power = ambientOcclusionPower
      ..detail = ambientOcclusionDetail
      ..horizonAngle = ambientOcclusionHorizonAngle
      ..directLightAffect = ambientOcclusionDirectLightAffect
      ..sampleCount = ambientOcclusionSampleCount
      ..halfResolution = ambientOcclusionHalfResolution
      ..depthMipChain = ambientOcclusionDepthMipChain
      ..specularMode = ambientOcclusionSpecularMode;
  }

  /// Interpolates from [a] to [b] by [t] (0 = [a], 1 = [b]). See the class doc
  /// for which fields blend and which switch.
  static EnvironmentSettings lerp(
    EnvironmentSettings a,
    EnvironmentSettings b,
    double t,
  ) {
    final pickB = t >= 0.5;
    EnvironmentSettings d = pickB ? b : a;
    return EnvironmentSettings(
      environment: d.environment,
      skybox: d.skybox,
      skyEnvironment: d.skyEnvironment,
      sunLight: d.sunLight,
      toneMapping: d.toneMapping,
      agxWhite: _lerp(a.agxWhite, b.agxWhite, t),
      agxContrast: _lerp(a.agxContrast, b.agxContrast, t),
      environmentIntensity: _lerp(
        a.environmentIntensity,
        b.environmentIntensity,
        t,
      ),
      exposure: _lerp(a.exposure, b.exposure, t),
      colorGradingEnabled: d.colorGradingEnabled,
      brightness: _lerp(a.brightness, b.brightness, t),
      contrast: _lerp(a.contrast, b.contrast, t),
      saturation: _lerp(a.saturation, b.saturation, t),
      temperature: _lerp(a.temperature, b.temperature, t),
      tint: _lerp(a.tint, b.tint, t),
      lift: _lerpVec3(a.lift, b.lift, t),
      gamma: _lerpVec3(a.gamma, b.gamma, t),
      gain: _lerpVec3(a.gain, b.gain, t),
      bloomEnabled: d.bloomEnabled,
      bloomThreshold: _lerp(a.bloomThreshold, b.bloomThreshold, t),
      bloomIntensity: _lerp(a.bloomIntensity, b.bloomIntensity, t),
      bloomScatter: _lerp(a.bloomScatter, b.bloomScatter, t),
      vignetteEnabled: d.vignetteEnabled,
      vignetteIntensity: _lerp(a.vignetteIntensity, b.vignetteIntensity, t),
      vignetteRadius: _lerp(a.vignetteRadius, b.vignetteRadius, t),
      vignetteSmoothness: _lerp(a.vignetteSmoothness, b.vignetteSmoothness, t),
      chromaticAberrationEnabled: d.chromaticAberrationEnabled,
      chromaticAberrationIntensity: _lerp(
        a.chromaticAberrationIntensity,
        b.chromaticAberrationIntensity,
        t,
      ),
      filmGrainEnabled: d.filmGrainEnabled,
      filmGrainIntensity: _lerp(a.filmGrainIntensity, b.filmGrainIntensity, t),
      ambientOcclusionEnabled: d.ambientOcclusionEnabled,
      ambientOcclusionRadius: _lerp(
        a.ambientOcclusionRadius,
        b.ambientOcclusionRadius,
        t,
      ),
      ambientOcclusionIntensity: _lerp(
        a.ambientOcclusionIntensity,
        b.ambientOcclusionIntensity,
        t,
      ),
      ambientOcclusionBias: _lerp(
        a.ambientOcclusionBias,
        b.ambientOcclusionBias,
        t,
      ),
      ambientOcclusionPower: _lerp(
        a.ambientOcclusionPower,
        b.ambientOcclusionPower,
        t,
      ),
      ambientOcclusionDetail: _lerp(
        a.ambientOcclusionDetail,
        b.ambientOcclusionDetail,
        t,
      ),
      ambientOcclusionHorizonAngle: _lerp(
        a.ambientOcclusionHorizonAngle,
        b.ambientOcclusionHorizonAngle,
        t,
      ),
      ambientOcclusionDirectLightAffect: _lerp(
        a.ambientOcclusionDirectLightAffect,
        b.ambientOcclusionDirectLightAffect,
        t,
      ),
      ambientOcclusionSampleCount: d.ambientOcclusionSampleCount,
      ambientOcclusionHalfResolution: d.ambientOcclusionHalfResolution,
      ambientOcclusionDepthMipChain: d.ambientOcclusionDepthMipChain,
      ambientOcclusionSpecularMode: d.ambientOcclusionSpecularMode,
    );
  }
}
