/**
 * Scene methods split from scene.js.
 */


/**
 * Add multiple directional lights from different angles to create definition
 * and depth without harsh shadows. This three-point lighting setup gives
 * surfaces varied illumination for better visual depth.
 */
beestat.component.scene.prototype.add_directional_lights_ = function() {
  // Prevent re-initialization if lights already exist
  if (this.directional_lights_ !== undefined) {
    return;
  }

  this.directional_lights_ = [];

  // Key light: Main light from upper front-right (strongest)
  const key_light = new THREE.DirectionalLight(0xffffff, beestat.component.scene.directional_light_intensity);
  key_light.position.set(2000, 3200, 2000);
  this.static_light_group_.add(key_light);
  this.directional_lights_.push(key_light);

  // Fill light: Softer light from upper front-left (balances key light)
  const fill_light = new THREE.DirectionalLight(0xffffff, beestat.component.scene.directional_light_intensity);
  fill_light.position.set(-2000, 2400, 2000);
  this.static_light_group_.add(fill_light);
  this.directional_lights_.push(fill_light);

  // Back light: Mild light from behind (creates rim lighting on edges)
  const back_light = new THREE.DirectionalLight(0xffffff, beestat.component.scene.directional_light_intensity);
  back_light.position.set(0, 2000, -2000);
  this.static_light_group_.add(back_light);
  this.directional_lights_.push(back_light);

  // Top light: Gentle overhead light for roof definition
  const top_light = new THREE.DirectionalLight(0xffffff, beestat.component.scene.directional_light_intensity);
  top_light.position.set(0, 4000, 0);
  this.static_light_group_.add(top_light);
  this.directional_lights_.push(top_light);

};


/**
 * Create static lights group containing ambient and directional fill lights.
 * These lights are always on and provide base illumination.
 */
beestat.component.scene.prototype.add_static_lights_ = function() {
  // Prevent re-initialization
  if (this.static_light_group_ !== undefined) {
    return;
  }

  // Initialize layers object if not already done
  if (this.layers_ === undefined) {
    this.layers_ = {};
  }

  this.static_light_group_ = new THREE.Group();
  this.scene_.add(this.static_light_group_);
  this.layers_['static_lights'] = this.static_light_group_;

  // Add ambient light
  this.ambient_light_ = new THREE.AmbientLight(
    0xffffff,
    beestat.component.scene.ambient_light_intensity
  );
  this.static_light_group_.add(this.ambient_light_);

  // Add directional fill lights
  this.add_directional_lights_();
};


/**
 * Directional sun and moon lights that provide natural lighting. Only
 * visible when the environment layer is enabled. Positions are calculated based
 * on time of day and location.
 */
beestat.component.scene.prototype.get_celestial_shadow_frustum_extent_ = function() {
  const bounding_box = this.get_scene_bounding_box_();
  const plan_width = Math.max(1, Number(bounding_box.right - bounding_box.left));
  const plan_height = Math.max(1, Number(bounding_box.bottom - bounding_box.top));
  const half_span = Math.max(plan_width, plan_height) / 2;
  const environment_padding = Math.max(0, beestat.component.scene.environment_padding || 0);
  const caster_margin = 420;
  // Extra scale keeps long low-angle shadows inside the ortho shadow camera.
  return Math.max(1000, (half_span + environment_padding + caster_margin) * 1.45);
};


/**
 * Configure a directional light shadow camera to cover the current scene size.
 *
 * @param {THREE.DirectionalLight} light
 */
beestat.component.scene.prototype.configure_celestial_shadow_camera_ = function(light) {
  const extent = this.get_celestial_shadow_frustum_extent_();
  light.shadow.camera.left = -extent;
  light.shadow.camera.right = extent;
  light.shadow.camera.top = extent;
  light.shadow.camera.bottom = -extent;
  light.shadow.camera.near = 0.5;
  light.shadow.camera.far = Math.max(5000, extent * 6);
  light.shadow.camera.updateProjectionMatrix();
};


/**
 * Directional sun and moon lights that provide natural lighting. Only
 * visible when the environment layer is enabled. Positions are calculated based
 * on time of day and location.
 */
beestat.component.scene.prototype.add_celestial_lights_ = function() {
  // Prevent re-initialization if lights already exist
  if (this.sun_light_ !== undefined) {
    return;
  }

  // Create celestial group if it doesn't exist
  if (this.celestial_light_group_ === undefined) {
    this.celestial_light_group_ = new THREE.Group();
    this.scene_.add(this.celestial_light_group_);
    this.layers_['celestial'] = this.celestial_light_group_;
  }

  // Sun light
  this.sun_light_ = new THREE.DirectionalLight(
    0xffffdd, // Slightly warm color for sunlight
    beestat.component.scene.sun_light_intensity
  );

  // Initial position (will be updated by update_celestial_lights_)
  this.sun_light_.position.set(500, 500, -500);

  // Enable shadow casting
  this.sun_light_.castShadow = true;
  this.sun_light_.shadow.mapSize.set(2048, 2048);
  this.sun_light_.shadow.bias = -0.001;

  // Configure shadow camera frustum based on scene size.
  this.configure_celestial_shadow_camera_(this.sun_light_);

  // Set target to world origin (0,0,0) so light always points there
  this.sun_light_.target.position.set(0, 0, 0);
  this.scene_.add(this.sun_light_.target);

  this.celestial_light_group_.add(this.sun_light_);

  // Faint arc showing the sun's path across the sky.
  this.sun_path_line_ = new THREE.Line(
    new THREE.BufferGeometry(),
    new THREE.LineBasicMaterial({
      'color': 0xffe7aa,
      'vertexColors': true,
      'transparent': true,
      'opacity': 0.14,
      'depthWrite': false
    })
  );
  this.sun_path_line_.layers.set(beestat.component.scene.layer_visible);
  this.celestial_light_group_.add(this.sun_path_line_);

  // Visible sun body and glow
  this.sun_visual_group_ = new THREE.Group();
  this.sun_visual_group_.layers.set(beestat.component.scene.layer_visible);
  this.celestial_light_group_.add(this.sun_visual_group_);

  this.sun_core_texture_ = this.create_sun_core_texture_();
  const sun_core_material = new THREE.SpriteMaterial({
    'map': this.sun_core_texture_,
    'color': 0xffffff,
    'transparent': true,
    'depthWrite': false,
    'depthTest': true,
    'opacity': 1
  });
  this.sun_core_mesh_ = new THREE.Sprite(sun_core_material);
  this.sun_core_mesh_.userData.is_celestial_object = true;
  this.sun_core_mesh_.scale.set(320, 320, 1);
  this.sun_core_mesh_.renderOrder = 12;
  this.sun_visual_group_.add(this.sun_core_mesh_);

  this.sun_glow_texture_ = this.create_sun_glow_texture_();
  const sun_glow_material = new THREE.SpriteMaterial({
    'map': this.sun_glow_texture_,
    'color': 0xfff0b0,
    'transparent': true,
    'blending': THREE.AdditiveBlending,
    'depthWrite': false,
    'depthTest': true,
    'opacity': 1
  });
  this.sun_glow_sprite_ = new THREE.Sprite(sun_glow_material);
  this.sun_glow_sprite_.userData.is_celestial_object = true;
  this.sun_glow_sprite_.scale.set(1037, 1037, 1);
  this.sun_glow_sprite_.renderOrder = 11;
  this.sun_visual_group_.add(this.sun_glow_sprite_);

  // Moon light
  this.moon_light_ = new THREE.DirectionalLight(
    0xaaccff, // Cool bluish color for moonlight
    beestat.component.scene.moon_light_intensity
  );

  // Initial position (will be updated by update_celestial_lights_)
  this.moon_light_.position.set(-500, 500, 500);

  // Enable shadow casting
  this.moon_light_.castShadow = true;
  this.moon_light_.shadow.mapSize.set(2048, 2048);
  this.moon_light_.shadow.bias = -0.001;

  // Configure shadow camera frustum based on scene size.
  this.configure_celestial_shadow_camera_(this.moon_light_);

  // Set target to world origin
  this.moon_light_.target.position.set(0, 0, 0);
  this.scene_.add(this.moon_light_.target);

  this.celestial_light_group_.add(this.moon_light_);

  // Visible moon disk with procedural phase texture.
  this.moon_visual_group_ = new THREE.Group();
  this.moon_visual_group_.layers.set(beestat.component.scene.layer_visible);
  this.celestial_light_group_.add(this.moon_visual_group_);

  this.update_moon_phase_texture_(0);
  const moon_material = new THREE.SpriteMaterial({
    'map': this.moon_phase_texture_,
    'transparent': true,
    'depthWrite': false,
    'depthTest': true,
    'opacity': 1
  });
  this.moon_sprite_ = new THREE.Sprite(moon_material);
  this.moon_sprite_.userData.is_celestial_object = true;
  this.moon_sprite_.scale.set(405, 405, 1);
  this.moon_visual_group_.add(this.moon_sprite_);

  this.add_stars_();
};


/**
 * Add stars as non-lighting visual sprites in the sky.
 */
beestat.component.scene.prototype.add_stars_ = function() {
  if (this.star_texture_ === undefined) {
    this.star_texture_ = this.create_star_texture_();
  }

  this.star_group_ = new THREE.Group();
  this.star_group_.layers.set(beestat.component.scene.layer_visible);
  this.celestial_light_group_.add(this.star_group_);

  this.stars_ = [];

  const radius = 4200;
  const star_density = Math.max(0, Number(this.get_scene_setting_('star_density') || 0));
  const star_count = Math.max(0, Math.round(1000 * star_density));
  for (let i = 0; i < star_count; i++) {
    const theta = Math.random() * Math.PI * 2;
    const phi = Math.acos((Math.random() * 2) - 1);

    const x = radius * Math.sin(phi) * Math.cos(theta);
    const y = radius * Math.cos(phi);
    const z = radius * Math.sin(phi) * Math.sin(theta);

    const size =
      beestat.component.scene.star_min_size +
      (Math.pow(Math.random(), 1.7) * (beestat.component.scene.star_max_size - beestat.component.scene.star_min_size));
    const is_twinkle = size >= 20;
    const base_opacity = 0.3 + (Math.random() * 0.7);

    const material = new THREE.SpriteMaterial({
      'map': this.star_texture_,
      'transparent': true,
      'opacity': 0,
      'depthWrite': false,
      'depthTest': true,
      'blending': THREE.AdditiveBlending
    });
    const star = new THREE.Sprite(material);
    star.position.set(x, y, z);
    star.scale.set(size, size, 1);
    star.userData.is_celestial_object = true;
    this.star_group_.add(star);

    this.stars_.push({
      'sprite': star,
      'base_opacity': base_opacity,
      'twinkle': is_twinkle,
      'twinkle_amount': is_twinkle ? (0.06 + (Math.random() * 0.1)) : 0,
      'twinkle_speed': is_twinkle ? (0.8 + (Math.random() * 1.2)) : 0,
      'phase': Math.random() * Math.PI * 2
    });
  }

  this.star_visibility_ = 0;
  this.target_star_visibility_ = 0;
};


/**
 * Update a faint line representing the sun's path for the current date and
 * location.
 *
 * @param {moment} date
 * @param {number} latitude
 * @param {number} longitude
 */
beestat.component.scene.prototype.update_sun_path_arc_ = function(date, latitude, longitude) {
  if (this.sun_path_line_ === undefined) {
    return;
  }

  const rotation_radians = (this.get_appearance_value_('rotation') * Math.PI) / 180;
  const sun_distance = 4000;
  const start_of_day = date.clone().startOf('day');
  const end_of_day = start_of_day.clone().add(1, 'day');
  const start_ms = start_of_day.valueOf();
  const end_ms = end_of_day.valueOf();
  const sample_count = 72;
  const points = [];

  for (let i = 0; i < sample_count; i++) {
    const t = i / (sample_count - 1);
    const sample_date = new Date(start_ms + (end_ms - start_ms) * t);

    const sun_pos = SunCalc.getPosition(sample_date, latitude, longitude);
    // Convert SunCalc azimuth (south-origin, west-positive) to a north-origin,
    // clockwise bearing, then apply floor-plan north rotation.
    const rotated_azimuth = sun_pos.azimuth + Math.PI + rotation_radians;

    // Extend slightly below horizon so the path doesn't hard-stop at horizon.
    if (sun_pos.altitude > -0.22) {
      points.push(new THREE.Vector3(
        sun_distance * Math.cos(sun_pos.altitude) * Math.sin(rotated_azimuth),
        sun_distance * Math.sin(sun_pos.altitude),
        -sun_distance * Math.cos(sun_pos.altitude) * Math.cos(rotated_azimuth)
      ));
    }
  }

  if (points.length >= 2) {
    const geometry = this.sun_path_line_.geometry;
    geometry.setFromPoints(points);

    // Fade the ends by dimming vertex colors toward each endpoint.
    const colors = [];
    const base = new THREE.Color(0xffe7aa);
    for (let i = 0; i < points.length; i++) {
      const t = i / (points.length - 1);
      const center_weight = Math.pow(Math.sin(Math.PI * t), 1.2);
      const intensity = 0.2 + (0.8 * center_weight);
      const color = base.clone().multiplyScalar(intensity);
      colors.push(color.r, color.g, color.b);
    }
    geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));

    this.sun_path_line_.visible = true;
  } else {
    this.sun_path_line_.geometry.setFromPoints([]);
    this.sun_path_line_.visible = false;
  }
};


/**
 * Build target sun colors from altitude.
 * Warmer near horizon and more neutral when the sun is high.
 *
 * @param {number} sun_altitude_radians
 *
 * @return {{light_color: THREE.Color, core_color: THREE.Color, glow_color: THREE.Color}}
 */
beestat.component.scene.prototype.get_sun_color_profile_ = function(sun_altitude_radians) {
  const warm_start_altitude = THREE.MathUtils.degToRad(-2);
  const warm_end_altitude = THREE.MathUtils.degToRad(26);
  const daylight_t = Math.max(
    0,
    Math.min(
      1,
      (sun_altitude_radians - warm_start_altitude) / Math.max(0.0001, warm_end_altitude - warm_start_altitude)
    )
  );
  const warmth = 1 - daylight_t;

  const light_day = new THREE.Color(0xfff5de);
  const light_horizon = new THREE.Color(0xffa66c);
  const core_day = new THREE.Color(0xffffff);
  const core_horizon = new THREE.Color(0xffcf98);
  const glow_day = new THREE.Color(0xfff0b0);
  const glow_horizon = new THREE.Color(0xff8a4f);

  return {
    'light_color': light_day.clone().lerp(light_horizon, warmth),
    'core_color': core_day.clone().lerp(core_horizon, warmth * 0.9),
    'glow_color': glow_day.clone().lerp(glow_horizon, warmth)
  };
};


/**
 * Update sun and moon light positions based on date and location using SunCalc.
 * Adjusts light intensities based on altitude and moon phase.
 *
 * @param {moment} date The date/time to calculate positions for
 * @param {number} latitude Location latitude
 * @param {number} longitude Location longitude
 * 
 * @link https://www.earthspacelab.com/app/solar-time/
 */
beestat.component.scene.prototype.update_celestial_lights_ = function(date, latitude, longitude) {
  const sun_distance = 4000;
  const moon_distance = 3920;
  const js_date = date.toDate();
  const rotation_radians = (this.get_appearance_value_('rotation') * Math.PI) / 180;

  // Sun
  const sun_pos = SunCalc.getPosition(js_date, latitude, longitude);
  // Convert SunCalc azimuth (south-origin, west-positive) to a north-origin,
  // clockwise bearing, then apply floor-plan north rotation.
  const rotated_sun_azimuth = sun_pos.azimuth + Math.PI + rotation_radians;
  this.sun_light_.position.set(
    sun_distance * Math.cos(sun_pos.altitude) * Math.sin(rotated_sun_azimuth),   // East-West
    sun_distance * Math.sin(sun_pos.altitude),                                // Up-Down (altitude)
    -sun_distance * Math.cos(sun_pos.altitude) * Math.cos(rotated_sun_azimuth)   // North-South
  );

  if (this.sun_visual_group_ !== undefined) {
    this.sun_visual_group_.position.copy(this.sun_light_.position);
    this.sun_visual_group_.visible = true;
    this.sun_visual_horizon_fade_ = Math.max(0, Math.min(1, (sun_pos.altitude + 0.15) / 0.3));
  }
  const sun_color_profile = this.get_sun_color_profile_(sun_pos.altitude);
  this.target_sun_light_color_ = sun_color_profile.light_color;
  this.target_sun_core_color_ = sun_color_profile.core_color;
  this.target_sun_glow_color_ = sun_color_profile.glow_color;

  const cloud_dimming = this.get_cloud_dimming_factor_();

  // Calculate target intensity for smooth transitions.
  // Keep the transition tight around the horizon so sunrise "pops in" with
  // the same quick behavior as sunset "drops out".
  const sun_transition_start_altitude = -0.015;
  const sun_transition_end_altitude = 0.075;
  const sun_horizon_visibility = Math.max(
    0,
    Math.min(
      1,
      (sun_pos.altitude - sun_transition_start_altitude) /
      Math.max(0.0001, sun_transition_end_altitude - sun_transition_start_altitude)
    )
  );
  const sun_intensity_factor = Math.pow(sun_horizon_visibility, 2.4);
  this.target_sun_intensity_ =
    beestat.component.scene.sun_light_intensity * sun_intensity_factor;
  this.target_sun_intensity_ *= cloud_dimming;

  // Fade stars out at day and in at night.
  this.target_star_visibility_ = Math.max(
    0,
    Math.min(1, (-sun_pos.altitude - 0.05) / 0.25)
  );

  const max_sun_intensity = Math.max(0.0001, Number(beestat.component.scene.sun_light_intensity || 0.0001));
  const normalized_sun_brightness = Math.max(
    0,
    Math.min(1, this.target_sun_intensity_ / max_sun_intensity)
  );
  const user_light_on_brightness_threshold = 0.34;
  const user_light_off_brightness_threshold = 0.42;
  if (this.user_lights_on_ === undefined) {
    this.user_lights_on_ = normalized_sun_brightness <= user_light_on_brightness_threshold;
  }
  if (this.user_lights_on_ === true) {
    if (normalized_sun_brightness >= user_light_off_brightness_threshold) {
      this.user_lights_on_ = false;
    }
  } else if (normalized_sun_brightness <= user_light_on_brightness_threshold) {
    this.user_lights_on_ = true;
  }
  this.target_light_source_intensity_multiplier_ = this.user_lights_on_ === true ? 1 : 0;

  // Moon
  const moon_pos = SunCalc.getMoonPosition(js_date, latitude, longitude);
  // Keep moon conversion consistent with the sun conversion.
  const rotated_moon_azimuth = moon_pos.azimuth + Math.PI + rotation_radians;
  const moon_illumination = SunCalc.getMoonIllumination(js_date);
  const moon_fraction = moon_illumination.fraction;
  const moon_phase = moon_illumination.phase;
  this.current_moon_fraction_ = moon_fraction;
  this.moon_light_.position.set(
    moon_distance * Math.cos(moon_pos.altitude) * Math.sin(rotated_moon_azimuth),    // East-West
    moon_distance * Math.sin(moon_pos.altitude),                                  // Up-Down (altitude)
    -moon_distance * Math.cos(moon_pos.altitude) * Math.cos(rotated_moon_azimuth)    // North-South
  );
  if (this.moon_visual_group_ !== undefined) {
    let moon_front_direction;
    if (this.camera_ !== undefined) {
      moon_front_direction = this.camera_.position.clone().sub(this.moon_light_.position).normalize();
    } else {
      moon_front_direction = this.moon_light_.position.clone().normalize().negate();
    }
    this.moon_visual_group_.position.copy(
      this.moon_light_.position.clone().add(moon_front_direction.multiplyScalar(20))
    );
    this.moon_visual_group_.visible = true;
    this.moon_visual_horizon_fade_ = Math.max(0, Math.min(1, (moon_pos.altitude + 0.12) / 0.24));

    if (this.last_moon_phase_ === undefined || Math.abs(this.last_moon_phase_ - moon_phase) > 0.002) {
      this.last_moon_phase_ = moon_phase;
      this.update_moon_phase_texture_(moon_phase);
    }
  }
  const moon_intensity = beestat.component.scene.moon_light_intensity * moon_fraction;

  this.update_sun_path_arc_(date, latitude, longitude);

  // Calculate target intensity for smooth transitions
  // Moon is only visible when sun is below horizon
  if (sun_pos.altitude >= 0) {
    this.target_moon_intensity_ = 0;
  } else {
    this.target_moon_intensity_ = moon_pos.altitude < 0
      ? Math.max(0, moon_intensity * (1 + moon_pos.altitude / (Math.PI / 6)))
      : moon_intensity;
  }
  this.target_moon_intensity_ *= cloud_dimming;
};


/**
 * Smoothly interpolate celestial light intensities towards their targets.
 * Called every frame to create smooth transitions instead of instant jumps.
 */
beestat.component.scene.prototype.update_celestial_light_intensities_ = function() {
  if (this.sun_light_ === undefined || this.moon_light_ === undefined) {
    return;
  }

  // Initialize current intensities if not set
  if (this.target_sun_intensity_ === undefined) {
    this.target_sun_intensity_ = 0;
  }
  if (this.target_moon_intensity_ === undefined) {
    this.target_moon_intensity_ = 0;
  }
  if (this.target_light_source_intensity_multiplier_ === undefined) {
    const hour = this.date_ !== undefined ? Number(this.date_.format('H')) : 12;
    this.target_light_source_intensity_multiplier_ = (hour >= 19 || hour <= 5) ? 1 : 0;
  }
  if (this.target_sun_light_color_ === undefined) {
    this.target_sun_light_color_ = new THREE.Color(0xffffdd);
  }
  if (this.target_sun_core_color_ === undefined) {
    this.target_sun_core_color_ = new THREE.Color(0xffffff);
  }
  if (this.target_sun_glow_color_ === undefined) {
    this.target_sun_glow_color_ = new THREE.Color(0xfff0b0);
  }

  // Lerp factor - lower = smoother but slower, higher = faster but jumpier
  const lerp_factor = 0.05;

  // Lerp sun intensity
  this.sun_light_.intensity += (this.target_sun_intensity_ - this.sun_light_.intensity) * lerp_factor;

  // Lerp moon intensity
  this.moon_light_.intensity += (this.target_moon_intensity_ - this.moon_light_.intensity) * lerp_factor;
  const color_lerp_factor = 0.08;
  this.sun_light_.color.lerp(this.target_sun_light_color_, color_lerp_factor);

  if (Array.isArray(this.light_sources_) === true) {
    this.light_sources_.forEach((light) => {
      const base_intensity = Number(light.userData.base_intensity || 0);
      const target_intensity = base_intensity * this.target_light_source_intensity_multiplier_;
      light.intensity += (target_intensity - light.intensity) * lerp_factor;
    });
  }
  // Match visible sun brightness to actual sun light intensity, with smooth
  // fade at/under the horizon.
  if (this.sun_core_mesh_ !== undefined && this.sun_glow_sprite_ !== undefined) {
    const max_sun_intensity = beestat.component.scene.sun_light_intensity;
    const intensity_ratio = max_sun_intensity > 0
      ? Math.max(0, Math.min(1, this.sun_light_.intensity / max_sun_intensity))
      : 0;
    const horizon_fade = this.sun_visual_horizon_fade_ !== undefined
      ? this.sun_visual_horizon_fade_
      : 1;
    const visual_strength = intensity_ratio * horizon_fade;

    this.sun_core_mesh_.material.opacity = Math.min(1, (0.65 + visual_strength * 0.8) * visual_strength);
    this.sun_glow_sprite_.material.opacity = Math.min(1, (0.45 + visual_strength * 1.4) * visual_strength);
    this.sun_core_mesh_.material.color.lerp(this.target_sun_core_color_, color_lerp_factor);
    this.sun_glow_sprite_.material.color.lerp(this.target_sun_glow_color_, color_lerp_factor);
  }

  if (this.moon_sprite_ !== undefined) {
    // Keep moon visible at all times. This visual opacity is phase-driven and
    // intentionally decoupled from moon light intensity and horizon fade.
    const moon_phase_visibility = Math.max(
      0.25,
      Math.min(1, Number(this.current_moon_fraction_ === undefined ? 1 : this.current_moon_fraction_))
    );
    this.moon_sprite_.material.opacity = Math.min(1, 0.08 + (moon_phase_visibility * 0.28));
    if (this.moon_visual_group_ !== undefined) {
      this.moon_visual_group_.visible = true;
    }
  }

  this.update_stars_();
};


/**
 * Update star visibility and subtle twinkle.
 */
beestat.component.scene.prototype.update_stars_ = function() {
  if (this.stars_ === undefined || this.stars_.length === 0) {
    return;
  }

  if (this.target_star_visibility_ === undefined) {
    this.target_star_visibility_ = 0;
  }
  if (this.star_visibility_ === undefined) {
    this.star_visibility_ = 0;
  }

  this.star_visibility_ += (this.target_star_visibility_ - this.star_visibility_) * 0.06;
  const visibility = Math.max(0, Math.min(1, this.star_visibility_));
  const now_seconds = window.performance.now() / 1000;

  if (this.star_group_ !== undefined) {
    if (this.date_ !== undefined && typeof this.date_.valueOf === 'function') {
      // Apparent star motion is westward due to Earth's eastward rotation.
      const sidereal_phase = (
        (this.date_.valueOf() / 1000) % beestat.component.scene.sidereal_day_seconds
      ) / beestat.component.scene.sidereal_day_seconds;
      this.star_group_.rotation.y = -sidereal_phase * Math.PI * 2 * beestat.component.scene.star_drift_visual_factor;
    }
    this.star_group_.visible = visibility > 0.005;
  }

  for (let i = 0; i < this.stars_.length; i++) {
    const star = this.stars_[i];
    let twinkle = 1;
    if (star.twinkle === true) {
      twinkle = 1 + (Math.sin((now_seconds * star.twinkle_speed) + star.phase) * star.twinkle_amount);
    }
    star.sprite.material.opacity = Math.max(
      0,
      Math.min(
        1,
        star.base_opacity * visibility * twinkle
      )
    );
  }
};


/**
 * Convert color temperature in Kelvin to RGB color.
 *
 * @param {number} temperature_k
 *
 * @return {THREE.Color}
 */
beestat.component.scene.prototype.get_light_color_from_temperature_ = function(temperature_k) {
  const kelvin = Math.max(1000, Math.min(12000, Number(temperature_k || 4000)));
  const temp = kelvin / 100;

  let red;
  let green;
  let blue;

  if (temp <= 66) {
    red = 255;
    green = 99.4708025861 * Math.log(temp) - 161.1195681661;
    blue = temp <= 19 ? 0 : (138.5177312231 * Math.log(temp - 10) - 305.0447927307);
  } else {
    red = 329.698727446 * Math.pow(temp - 60, -0.1332047592);
    green = 288.1221695283 * Math.pow(temp - 60, -0.0755148492);
    blue = 255;
  }

  const clamp_channel = function(value) {
    return Math.max(0, Math.min(255, Number(value || 0)));
  };

  return new THREE.Color(
    clamp_channel(red) / 255,
    clamp_channel(green) / 255,
    clamp_channel(blue) / 255
  );
};


/**
 * Add floor-plan light sources.
 *
 * @param {THREE.Group} layer The layer to add light sources to.
 * @param {object} group The floor plan group.
 */
beestat.component.scene.prototype.add_light_sources_ = function(layer, group) {
  if (this.get_scene_setting_('light_user_enabled') !== true) {
    return;
  }

  if (Array.isArray(group.light_sources) !== true || group.light_sources.length === 0) {
    return;
  }
  if (Array.isArray(this.light_sources_) !== true) {
    this.light_sources_ = [];
  }

  const group_elevation = Number(group.elevation || 0);
  const floor_thickness = Number(beestat.component.scene.room_floor_thickness || 0);
  const user_light_cast_shadows = this.get_scene_setting_('light_user_cast_shadows') === true;

  group.light_sources.forEach(function(light_source) {
    const x = Number(light_source.x || 0);
    const y = Number(light_source.y || 0);
    const elevation = Number(light_source.elevation !== undefined ? light_source.elevation : 72);
    const z = -group_elevation - floor_thickness - elevation;
    let intensity_level = 2;
    if (light_source.intensity === 'dim') {
      intensity_level = 1;
    } else if (light_source.intensity === 'bright') {
      intensity_level = 3;
    }
    const light_intensity = 0.9 * intensity_level;
    const light_color = this.get_light_color_from_temperature_(light_source.temperature_k);

    const light = new THREE.PointLight(light_color, light_intensity, 240, 2);
    light.userData.base_intensity = light_intensity;
    light.intensity = light_intensity * Number(this.target_light_source_intensity_multiplier_ || 0);
    light.position.set(x, y, z);
    light.castShadow = user_light_cast_shadows;
    if (user_light_cast_shadows === true) {
      light.shadow.mapSize.width = 512;
      light.shadow.mapSize.height = 512;
      light.shadow.bias = -0.0012;
      light.shadow.normalBias = 0.025;
      light.shadow.radius = 2;
      light.shadow.camera.near = 1;
      light.shadow.camera.far = 240;
    }
    light.userData.is_light_source = true;
    layer.add(light);
    this.light_sources_.push(light);
  }, this);
};


/**
 * Apply the current user-light shadow setting to existing user lights.
 */
beestat.component.scene.prototype.update_user_light_shadow_settings_ = function() {
  if (Array.isArray(this.light_sources_) !== true) {
    return;
  }

  const user_light_cast_shadows = this.get_scene_setting_('light_user_cast_shadows') === true;
  this.light_sources_.forEach(function(light) {
    if (light === undefined || light === null || light.isPointLight !== true) {
      return;
    }

    light.castShadow = user_light_cast_shadows;
    if (user_light_cast_shadows === true) {
      light.shadow.mapSize.width = 512;
      light.shadow.mapSize.height = 512;
      light.shadow.bias = -0.0012;
      light.shadow.normalBias = 0.025;
      light.shadow.radius = 2;
      light.shadow.camera.near = 1;
      light.shadow.camera.far = 240;
    }
  });

  if (this.renderer_ !== undefined && this.renderer_.shadowMap !== undefined) {
    this.renderer_.shadowMap.needsUpdate = true;
  }
};



