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


/**
 * Get the ceiling Z-position for a room.
 *
 * @param {object} group The floor plan group
 * @param {object} room The room
 *
 * @return {number} The ceiling Z position
 */
beestat.component.scene.prototype.get_ceiling_z_ = function(group, room) {
  const elevation = room.elevation || group.elevation || 0;
  const height = room.height || group.height || 96;
  return -(elevation + height);
};


/**
 * Convert room.points (relative coordinates) to absolute coordinates.
 *
 * @param {object} room The room
 *
 * @return {Array} Array of absolute coordinate points {x, y}
 */
beestat.component.scene.prototype.convert_room_to_absolute_polygon_ = function(room) {
  const absolute = [];
  room.points.forEach(function(point) {
    absolute.push({
      'x': room.x + point.x,
      'y': room.y + point.y
    });
  });
  return absolute;
};


/**
 * Compute which ceiling areas are exposed (not covered by floors above).
 *
 * @param {object} floor_plan The floor plan
 *
 * @return {Array} Array of {ceiling_z, polygons[]} for roof outline rendering
 */
beestat.component.scene.prototype.compute_exposed_ceiling_areas_ = function(floor_plan) {
  const self = this;

  // Step 1: Group ceilings by Z-level
  const ceiling_levels = {}; // Key: ceiling_z, Value: array of room polygons

  floor_plan.data.groups.forEach(function(group) {
    group.rooms.forEach(function(room) {
      const elevation = room.elevation || group.elevation || 0;

      // Skip basements (below ground)
      if (elevation < 0) {
        return;
      }

      const ceiling_z = self.get_ceiling_z_(group, room);

      if (!ceiling_levels[ceiling_z]) {
        ceiling_levels[ceiling_z] = [];
      }

      ceiling_levels[ceiling_z].push(
        self.convert_room_to_absolute_polygon_(room)
      );
    });
  });

  // Step 2: Sort ceiling levels (ascending Z = highest to lowest)
  const sorted_levels = Object.keys(ceiling_levels)
    .map(z => parseFloat(z))
    .sort((a, b) => a - b);

  const exposed_areas = [];

  // Step 3: For each level, compute exposed area
  sorted_levels.forEach(function(current_ceiling_z, index) {
    const current_polygons = ceiling_levels[current_ceiling_z];

    // Union all rooms at this level
    const union_clipper = new ClipperLib.Clipper();
    current_polygons.forEach(function(polygon) {
      union_clipper.AddPath(polygon, ClipperLib.PolyType.ptSubject, true);
    });

    const ceiling_area = new ClipperLib.Paths();
    union_clipper.Execute(
      ClipperLib.ClipType.ctUnion,
      ceiling_area,
      ClipperLib.PolyFillType.pftNonZero,
      ClipperLib.PolyFillType.pftNonZero
    );

    // Compute occlusion from all higher levels
    const occlusion_clipper = new ClipperLib.Clipper();
    let has_occlusion = false;

    for (let i = 0; i < index; i++) {
      const above_ceiling_z = sorted_levels[i];
      const above_polygons = ceiling_levels[above_ceiling_z];

      above_polygons.forEach(function(polygon) {
        occlusion_clipper.AddPath(polygon, ClipperLib.PolyType.ptSubject, true);
        has_occlusion = true;
      });
    }

    let exposed;

    if (!has_occlusion) {
      // Top floor - no occlusion, entire ceiling is exposed
      exposed = ceiling_area;
    } else {
      // Compute union of all occlusion polygons
      const occlusion_area = new ClipperLib.Paths();
      occlusion_clipper.Execute(
        ClipperLib.ClipType.ctUnion,
        occlusion_area,
        ClipperLib.PolyFillType.pftNonZero,
        ClipperLib.PolyFillType.pftNonZero
      );

      // Subtract occlusion from ceiling
      const diff_clipper = new ClipperLib.Clipper();
      ceiling_area.forEach(function(path) {
        diff_clipper.AddPath(path, ClipperLib.PolyType.ptSubject, true);
      });
      occlusion_area.forEach(function(path) {
        diff_clipper.AddPath(path, ClipperLib.PolyType.ptClip, true);
      });

      exposed = new ClipperLib.Paths();
      diff_clipper.Execute(
        ClipperLib.ClipType.ctDifference,
        exposed,
        ClipperLib.PolyFillType.pftNonZero,
        ClipperLib.PolyFillType.pftNonZero
      );
    }

    // Filter out tiny polygons (floating-point artifacts)
    const filtered = exposed.filter(function(path) {
      return Math.abs(ClipperLib.Clipper.Area(path)) > 1;
    });

    if (filtered.length > 0) {
      exposed_areas.push({
        'ceiling_z': current_ceiling_z,
        'polygons': filtered
      });
    }
  });

  return exposed_areas;
};


/**
 * Return whether the current scene date is Christmas Day (12/25).
 *
 * @return {boolean}
 */
beestat.component.scene.prototype.is_christmas_day_ = function() {
  if (
    this.date_ === undefined ||
    typeof this.date_.month !== 'function' ||
    typeof this.date_.date !== 'function'
  ) {
    return false;
  }

  return this.date_.month() === 11 && this.date_.date() === 25;
};


/**
 * Toggle Christmas roofline lights visibility based on active date.
 */
beestat.component.scene.prototype.update_christmas_lights_visibility_ = function() {
  if (this.christmas_lights_group_ === undefined) {
    return;
  }

  this.christmas_lights_group_.visible = this.is_christmas_day_();
};


/**
 * Add cheap Christmas roofline bulbs along resolved roof perimeter edges.
 *
 * @param {THREE.Group} roofs_layer
 * @param {Array<{z: number, polygon: Array<{x:number,y:number}>}>} roofline_paths
 */
beestat.component.scene.prototype.add_christmas_lights_ = function(roofs_layer, roofline_paths) {
  if (
    roofs_layer === undefined ||
    Array.isArray(roofline_paths) !== true ||
    roofline_paths.length === 0
  ) {
    return;
  }

  const spacing = Math.max(8, Number(beestat.component.scene.christmas_light_spacing || 28));
  const bulb_radius = Math.max(1, Number(beestat.component.scene.christmas_light_size || 4.2));
  const colors = Array.isArray(beestat.component.scene.christmas_light_colors) === true &&
    beestat.component.scene.christmas_light_colors.length > 0
    ? beestat.component.scene.christmas_light_colors
    : [0xff2b2b, 0x34d44e, 0x3f7bff, 0xffd93a];
  const bulb_z_offset = 2.5;

  const positions_by_color = {};
  colors.forEach(function(color) {
    positions_by_color[color] = [];
  });

  let bulb_index = 0;
  roofline_paths.forEach(function(path) {
    const polygon = path.polygon;
    if (Array.isArray(polygon) !== true || polygon.length < 2) {
      return;
    }

    for (let i = 0; i < polygon.length; i++) {
      const a = polygon[i];
      const b = polygon[(i + 1) % polygon.length];
      const dx = b.x - a.x;
      const dy = b.y - a.y;
      const length = Math.sqrt((dx * dx) + (dy * dy));
      const count = Math.max(1, Math.round(length / spacing));

      for (let j = 0; j < count; j++) {
        const t = j / count;
        const x = a.x + (dx * t);
        const y = a.y + (dy * t);
        const z = path.z - bulb_z_offset;
        const color = colors[bulb_index % colors.length];
        positions_by_color[color].push({'x': x, 'y': y, 'z': z});
        bulb_index++;
      }
    }
  });

  if (bulb_index === 0) {
    return;
  }

  const christmas_group = new THREE.Group();
  christmas_group.userData.is_roof_christmas_lights = true;
  christmas_group.layers.set(beestat.component.scene.layer_visible);

  const matrix = new THREE.Matrix4();
  const bulb_geometry = new THREE.SphereGeometry(bulb_radius, 6, 5);
  colors.forEach(function(color) {
    const positions = positions_by_color[color];
    if (Array.isArray(positions) !== true || positions.length === 0) {
      return;
    }

    const material = new THREE.MeshBasicMaterial({
      'color': color,
      'toneMapped': false
    });
    const bulbs = new THREE.InstancedMesh(bulb_geometry, material, positions.length);
    bulbs.castShadow = false;
    bulbs.receiveShadow = false;
    bulbs.layers.set(beestat.component.scene.layer_visible);

    for (let i = 0; i < positions.length; i++) {
      const position = positions[i];
      matrix.makeTranslation(position.x, position.y, position.z);
      bulbs.setMatrixAt(i, matrix);
    }
    bulbs.instanceMatrix.needsUpdate = true;
    christmas_group.add(bulbs);
  });

  roofs_layer.add(christmas_group);
  this.christmas_lights_group_ = christmas_group;
  this.update_christmas_lights_visibility_();
};


/**
 * Add roofs to the scene based on the configured roof style.
 */
beestat.component.scene.prototype.add_roofs_ = function() {
  // Resolve configured roof mode and available skeleton runtime.
  const skeleton_builder = this.get_skeleton_builder_();
  const roof_style = this.get_appearance_value_('roof_style');

  // Prefer requested roof style; fall back to flat until skeleton runtime is ready.
  if (roof_style === 'flat') {
    this.add_flat_roofs_();
  } else if (roof_style === 'hip' && skeleton_builder !== undefined) {
    this.add_hip_roofs_(skeleton_builder);
  } else {
    if (roof_style === 'hip') {
      this.listen_for_skeleton_builder_ready_();
    }
    this.add_flat_roofs_();
  }
};


/**
 * Add hip roofs using the straight skeleton algorithm.
 *
 * @param {object} skeleton_builder
 */
beestat.component.scene.prototype.add_hip_roofs_ = function(skeleton_builder) {
  // Gather exposed ceiling polygons and shared roof style settings.
  const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
  const exposed_areas = this.compute_exposed_ceiling_areas_(floor_plan);
  const roof_color = this.get_appearance_value_('roof_color');

  // Create layer for generated roof meshes.
  const roofs_layer = new THREE.Group();
  this.floor_plan_group_.add(roofs_layer);
  this.layers_['roof'] = roofs_layer;

  const roof_pitch = beestat.component.scene.roof_pitch;
  const roofline_paths = [];

  // Build hip roof geometry per exposed polygon.
  exposed_areas.forEach(function(area) {
    area.polygons.forEach(function(polygon) {
      if (polygon.length < 3) {
        return;
      }

      try {
        // Normalize polygon topology before offset/skeleton operations.
        const simplified = ClipperLib.Clipper.SimplifyPolygon(
          polygon,
          ClipperLib.PolyFillType.pftNonZero
        );

        simplified.forEach(function(simple_polygon) {
          if (simple_polygon.length < 3) {
            return;
          }

          // Expand polygon to add overhang around the exposed ceiling footprint.
          const roof_overhang = beestat.component.scene.roof_overhang;
          const clipper_offset = new ClipperLib.ClipperOffset();
          clipper_offset.AddPath(
            simple_polygon,
            ClipperLib.JoinType.jtMiter,
            ClipperLib.EndType.etClosedPolygon
          );
          const offset_polygons = new ClipperLib.Paths();
          clipper_offset.Execute(offset_polygons, roof_overhang);

          // Use the offset polygon if successful, otherwise use original
          const roof_polygon = (offset_polygons.length > 0) ? offset_polygons[0] : simple_polygon;
          roofline_paths.push({
            'z': area.ceiling_z,
            'polygon': roof_polygon
          });

          // Add a thin base skirt so eaves have subtle physical thickness.
          const base_shape = new THREE.Shape();
          base_shape.moveTo(roof_polygon[0].x, roof_polygon[0].y);
          for (let i = 1; i < roof_polygon.length; i++) {
            base_shape.lineTo(roof_polygon[i].x, roof_polygon[i].y);
          }
          base_shape.closePath();

          const hip_roof_base_thickness = 4;
          const base_geometry = new THREE.ExtrudeGeometry(base_shape, {
            'depth': hip_roof_base_thickness,
            'bevelEnabled': false
          });
          const base_material = new THREE.MeshStandardMaterial({
            'color': roof_color,
            'side': THREE.DoubleSide,
            'flatShading': false,
            'roughness': 0.85,
            'metalness': 0.0
          });
          const base_mesh = new THREE.Mesh(base_geometry, base_material);
          // Nudge downward so the top cap doesn't z-fight with hip roof faces.
          base_mesh.position.z = area.ceiling_z + 0.5;
          base_mesh.userData.is_roof = true;
          base_mesh.layers.set(beestat.component.scene.layer_visible);
          base_mesh.castShadow = true;
          base_mesh.receiveShadow = true;
          roofs_layer.add(base_mesh);

          // Convert polygon into straight-skeleton input format.
          const ring = roof_polygon.map(function(point) {
            return [point.x, point.y];
          });
          ring.push([roof_polygon[0].x, roof_polygon[0].y]);

          const coordinates = [ring];
          const result = skeleton_builder.buildFromPolygon(coordinates);

          if (!result) {
            return;
          }

          // Boundary vertices stay at ceiling level; interior vertices get raised by pitch.
          const boundary_vertex_count = roof_polygon.length;
          const boundary_set = new Set();
          for (let i = 0; i < boundary_vertex_count; i++) {
            boundary_set.add(i);
          }

          // Helper: compute shortest distance from a point to roof footprint edges.
          const compute_distance_to_boundary = function(point_x, point_y) {
            let min_distance = Infinity;

            for (let i = 0; i < roof_polygon.length; i++) {
              const p1 = roof_polygon[i];
              const p2 = roof_polygon[(i + 1) % roof_polygon.length];

              // Calculate perpendicular distance from point to line segment
              const dx = p2.x - p1.x;
              const dy = p2.y - p1.y;
              const length_sq = dx * dx + dy * dy;

              if (length_sq === 0) {
                // Point to point distance
                const dist = Math.sqrt(
                  Math.pow(point_x - p1.x, 2) + Math.pow(point_y - p1.y, 2)
                );
                min_distance = Math.min(min_distance, dist);
                continue;
              }

              // Project point onto line segment
              let t = ((point_x - p1.x) * dx + (point_y - p1.y) * dy) / length_sq;
              t = Math.max(0, Math.min(1, t));

              const closest_x = p1.x + t * dx;
              const closest_y = p1.y + t * dy;

              const dist = Math.sqrt(
                Math.pow(point_x - closest_x, 2) + Math.pow(point_y - closest_y, 2)
              );

              min_distance = Math.min(min_distance, dist);
            }

            return min_distance;
          };

          // Lift interior skeleton vertices to form sloped hip planes.
          const vertices_3d = result.vertices.map(function(vertex, index) {
            const is_boundary = boundary_set.has(index);
            let height = 0;

            if (!is_boundary) {
              // Interior skeleton vertex - raise it based on distance to boundary
              const distance = compute_distance_to_boundary(vertex[0], vertex[1]);
              height = distance * roof_pitch;
            }

            return new THREE.Vector3(
              vertex[0],
              vertex[1],
              area.ceiling_z - height  // Negative Z = higher in world coords
            );
          });

          // Triangulate each skeleton face and emit a renderable mesh.
          result.polygons.forEach(function(face) {
            if (face.length < 3) {
              return;
            }

            // Create triangulated mesh for this face
            const face_vertices = face.map(function(idx) {
              return vertices_3d[idx];
            });

            // Triangulate the face (simple fan triangulation from first vertex)
            const triangles = [];
            for (let i = 1; i < face_vertices.length - 1; i++) {
              triangles.push(
                face_vertices[0],
                face_vertices[i],
                face_vertices[i + 1]
              );
            }

            // Create geometry
            const geometry = new THREE.BufferGeometry().setFromPoints(triangles);
            geometry.computeVertexNormals();

            // Create material - use appearance roof color
            const material = new THREE.MeshStandardMaterial({
              'color': roof_color,
              'side': THREE.DoubleSide,
              'flatShading': false,
              'roughness': 0.8,
              'metalness': 0.0
            });

            const mesh = new THREE.Mesh(geometry, material);
            mesh.userData.is_roof = true;
            mesh.layers.set(beestat.component.scene.layer_visible);
            mesh.castShadow = true;
            mesh.receiveShadow = true;
            roofs_layer.add(mesh);
          });
        });
      } catch (error) {
        console.error('Error generating roof:', error, polygon);
      }
    });
  });

  this.add_christmas_lights_(roofs_layer, roofline_paths);
};


/**
 * Add flat roofs to the scene.
 */
beestat.component.scene.prototype.add_flat_roofs_ = function() {
  // Gather exposed ceiling polygons and shared roof style settings.
  const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
  const exposed_areas = this.compute_exposed_ceiling_areas_(floor_plan);
  const roof_color = this.get_appearance_value_('roof_color');

  // Create layer for generated roof meshes.
  const roofs_layer = new THREE.Group();
  this.floor_plan_group_.add(roofs_layer);
  this.layers_['roof'] = roofs_layer;
  const roofline_paths = [];

  // Build flat roof geometry per exposed polygon.
  exposed_areas.forEach(function(area) {
    area.polygons.forEach(function(polygon) {
      if (polygon.length < 3) {
        return;
      }

      try {
        // Normalize polygon topology before offset/extrusion.
        const simplified = ClipperLib.Clipper.SimplifyPolygon(
          polygon,
          ClipperLib.PolyFillType.pftNonZero
        );

        simplified.forEach(function(simple_polygon) {
          if (simple_polygon.length < 3) {
            return;
          }

          // Expand polygon to add overhang around the exposed ceiling footprint.
          const roof_overhang = beestat.component.scene.roof_overhang;
          const clipper_offset = new ClipperLib.ClipperOffset();
          clipper_offset.AddPath(
            simple_polygon,
            ClipperLib.JoinType.jtMiter,
            ClipperLib.EndType.etClosedPolygon
          );
          const offset_polygons = new ClipperLib.Paths();
          clipper_offset.Execute(offset_polygons, roof_overhang);

          // Use the offset polygon if successful, otherwise use original
          const roof_polygon = (offset_polygons.length > 0) ? offset_polygons[0] : simple_polygon;
          roofline_paths.push({
            'z': area.ceiling_z,
            'polygon': roof_polygon
          });

          // Build the flat roof footprint shape for extrusion.
          const shape = new THREE.Shape();
          shape.moveTo(roof_polygon[0].x, roof_polygon[0].y);
          for (let i = 1; i < roof_polygon.length; i++) {
            shape.lineTo(roof_polygon[i].x, roof_polygon[i].y);
          }
          shape.closePath();

          // Extrude the footprint so the flat roof has physical depth.
          const flat_roof_depth = 6; // 6 inches of depth
          const geometry = new THREE.ExtrudeGeometry(shape, {
            'depth': flat_roof_depth,
            'bevelEnabled': false
          });

          // Create material - use appearance roof color
          const material = new THREE.MeshStandardMaterial({
            'color': roof_color,
            'side': THREE.DoubleSide,
            'flatShading': false,
            'roughness': 0.9,  // Slightly higher roughness for flat roofs
            'metalness': 0.0
          });

          const mesh = new THREE.Mesh(geometry, material);
          mesh.position.z = area.ceiling_z - flat_roof_depth;  // Position so top is at ceiling level
          mesh.userData.is_roof = true;
          mesh.layers.set(beestat.component.scene.layer_visible);
          mesh.castShadow = true;
          mesh.receiveShadow = true;

          roofs_layer.add(mesh);
        });
      } catch (error) {
        console.error('Error generating flat roof:', error, polygon);
      }
    });
  });

  this.add_christmas_lights_(roofs_layer, roofline_paths);
};


/**
 * Get the straight-skeleton runtime when it has finished initializing.
 *
 * @return {object|undefined}
 */
beestat.component.scene.prototype.get_skeleton_builder_ = function() {
  if (window.SkeletonBuilderInitialized === true) {
    return window.SkeletonBuilder;
  }
  return undefined;
};


/**
 * If the skeleton runtime is still loading, listen for readiness and rerender
 * once so hip roofs replace fallback flat roofs.
 */
beestat.component.scene.prototype.listen_for_skeleton_builder_ready_ = function() {
  const self = this;

  if (this.skeleton_builder_ready_handler_ !== undefined) {
    return;
  }

  this.skeleton_builder_ready_handler_ = function() {
    if (self.skeleton_builder_ready_handler_ !== undefined) {
      window.removeEventListener('skeleton_builder_ready', self.skeleton_builder_ready_handler_);
      delete self.skeleton_builder_ready_handler_;
    }

    if (self.rendered_ === true) {
      self.rerender();
    }
  };

  window.addEventListener('skeleton_builder_ready', this.skeleton_builder_ready_handler_);
};
