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


/**
 * Build an opening cutter mesh for CSG subtraction.
 *
 * @param {object} group The floor plan group.
 * @param {object} opening The opening.
 * @return {?THREE.Mesh} Opening cutter mesh or null if opening is not cuttable.
 */
beestat.component.scene.prototype.build_opening_cutter_mesh_ = function(group, opening) {
  const opening_line = this.get_opening_line_params_(opening);
  const width = opening_line.width;
  const height = Math.max(1, Number(opening.height || this.get_opening_default_height_(opening.type)));
  const wall_thickness = Number(beestat.component.scene.wall_thickness || 4);
  // Slightly oversize cutter depth so CSG fully clears wall thickness even when
  // a snapped opening is numerically near-coplanar with one wall face.
  const cutter_depth_padding = 4;
  const depth = Math.max(0.5, wall_thickness + cutter_depth_padding);
  const center_z = this.get_opening_center_z_(group, opening, height);

  if (this.csg_cutter_material_ === undefined) {
    this.csg_cutter_material_ = new THREE.MeshBasicMaterial({
      'visible': false
    });
  }

  const geometry = new THREE.BoxGeometry(width, depth, height);
  const cutter = new THREE.Mesh(geometry, this.csg_cutter_material_);
  cutter.position.set(
    opening_line.center_x,
    opening_line.center_y,
    center_z
  );
  cutter.rotation.z = opening_line.rotation_radians;
  cutter.updateMatrix();
  cutter.updateMatrixWorld(true);

  return cutter;
};


/**
 * Whether an opening type should be treated as glass-family geometry.
 * Window reuses glass geometry and only adds a crossbar.
 *
 * @param {string} type
 *
 * @return {boolean}
 */
beestat.component.scene.prototype.is_opening_glass_family_ = function(type) {
  return type === 'window' || type === 'glass';
};


/**
 * Get default opening width by type.
 *
 * @param {string} type
 *
 * @return {number}
 */
beestat.component.scene.prototype.get_opening_default_width_ = function(type) {
  return this.is_opening_glass_family_(type) ? 48 : 36;
};


/**
 * Get default opening height by type.
 *
 * @param {string} type
 *
 * @return {number}
 */
beestat.component.scene.prototype.get_opening_default_height_ = function(type) {
  return this.is_opening_glass_family_(type) ? 60 : 78;
};


/**
 * Get default opening elevation by type.
 *
 * @param {string} type
 *
 * @return {number}
 */
beestat.component.scene.prototype.get_opening_default_elevation_ = function(type) {
  return this.is_opening_glass_family_(type) ? 24 : 0;
};


/**
 * Resolve opening line parameters from endpoint data.
 *
 * @param {object} opening
 *
 * @return {{center_x:number, center_y:number, width:number, rotation_radians:number}}
 */
beestat.component.scene.prototype.get_opening_line_params_ = function(opening) {
  const points = (
    opening.points !== undefined &&
    Array.isArray(opening.points) === true &&
    opening.points.length === 2
  )
    ? opening.points
    : null;

  let p1;
  let p2;
  if (points !== null) {
    p1 = points[0];
    p2 = points[1];
  } else {
    const center_x = Number(opening.x || 0);
    const center_y = Number(opening.y || 0);
    const width = Math.max(12, Number(opening.width || this.get_opening_default_width_(opening.type)));
    const half_width = width / 2;
    p1 = {
      'x': center_x - half_width,
      'y': center_y
    };
    p2 = {
      'x': center_x + half_width,
      'y': center_y
    };
  }

  const dx = Number(p2.x || 0) - Number(p1.x || 0);
  const dy = Number(p2.y || 0) - Number(p1.y || 0);
  return {
    'center_x': (Number(p1.x || 0) + Number(p2.x || 0)) / 2,
    'center_y': (Number(p1.y || 0) + Number(p2.y || 0)) / 2,
    'width': Math.max(12, Math.sqrt((dx * dx) + (dy * dy))),
    'rotation_radians': Math.atan2(dy, dx)
  };
};


/**
 * Get the opening center Z. Opening elevation is measured from the bottom of
 * the room reference plane, matching existing floor-plan behavior.
 *
 * @param {object} group The floor plan group.
 * @param {object} opening The opening.
 * @param {number} height The opening height.
 *
 * @return {number}
 */
beestat.component.scene.prototype.get_opening_center_z_ = function(group, opening, height) {
  const group_elevation = Number(group.elevation || 0);
  const floor_thickness = Number(beestat.component.scene.room_floor_thickness || 0);
  const opening_elevation = Number(
    opening.elevation !== undefined
      ? opening.elevation
      : this.get_opening_default_elevation_(opening.type)
  );
  return -group_elevation - floor_thickness - opening_elevation - (height / 2);
};


/**
 * Subtract opening cutters from wall meshes.
 *
 * @param {THREE.Group} walls_layer The wall mesh layer.
 * @param {object} floor_plan The floor plan data.
 */
beestat.component.scene.prototype.apply_opening_cuts_ = function(
  walls_layer,
  floor_plan
) {
  if (window.CSG === undefined || typeof window.CSG.subtract !== 'function') {
    return;
  }

  const wall_meshes = walls_layer.children.filter(function(child) {
    return (
      child !== undefined &&
      child.type === 'Mesh' &&
      child.userData !== undefined &&
      child.userData.wall_cuttable === true
    );
  });

  floor_plan.data.groups.forEach(function(group) {
    const openings = group.openings || [];
    if (openings.length === 0) {
      return;
    }

    const group_wall_meshes = wall_meshes.filter(function(mesh) {
      return mesh.userData.group_id === group.group_id;
    });
    if (group_wall_meshes.length === 0) {
      return;
    }

    openings.forEach((opening) => {
      const cutter = this.build_opening_cutter_mesh_(group, opening);
      if (cutter === null) {
        return;
      }

      const cutter_box = new THREE.Box3().setFromObject(cutter);

      group_wall_meshes.forEach(function(wall_mesh) {
        const wall_box = new THREE.Box3().setFromObject(wall_mesh);
        if (wall_box.intersectsBox(cutter_box) !== true) {
          return;
        }

        try {
          wall_mesh.updateMatrix();
          wall_mesh.updateMatrixWorld(true);

          const result_mesh = window.CSG.subtract(wall_mesh, cutter);
          if (
            result_mesh === undefined ||
            result_mesh.geometry === undefined ||
            result_mesh.geometry.attributes === undefined ||
            result_mesh.geometry.attributes.position === undefined ||
            result_mesh.geometry.attributes.position.count === 0
          ) {
            return;
          }

          result_mesh.geometry.computeBoundingBox();
          result_mesh.geometry.computeBoundingSphere();
          result_mesh.geometry.computeVertexNormals();

          const old_geometry = wall_mesh.geometry;
          wall_mesh.geometry = result_mesh.geometry;
          wall_mesh.castShadow = true;
          wall_mesh.receiveShadow = true;
          wall_mesh.layers.set(beestat.component.scene.layer_visible);
          wall_mesh.updateMatrix();
          wall_mesh.updateMatrixWorld(true);

          if (old_geometry !== undefined) {
            old_geometry.dispose();
          }
        } catch (error) {
          // Keep original wall mesh if CSG subtraction fails.
        }
      });

      cutter.geometry.dispose();
    });
  }, this);
};


/**
 * Add 3D opening fixtures.
 *
 * @param {THREE.Group} layer The layer to add fixtures to.
 * @param {object} group The floor plan group.
 */
beestat.component.scene.prototype.add_opening_fixtures_ = function(layer, group) {
  if (group.openings === undefined || group.openings.length === 0) {
    return;
  }

  const wall_thickness = Number(beestat.component.scene.wall_thickness || 4);
  const frame_thickness = Math.max(1, wall_thickness * 0.3);

  if (this.opening_frame_material_ === undefined) {
    this.opening_frame_material_ = new THREE.MeshStandardMaterial({
      'color': 0xf5f7fb,
      'roughness': 0.92,
      'metalness': 0.0
    });
  }
  if (this.window_pane_material_ === undefined) {
    this.window_pane_material_ = new THREE.MeshPhysicalMaterial({
      'color': 0xcfe0ee,
      'transparent': true,
      'opacity': 0.95,
      'roughness': 0.12,
      'metalness': 0.0,
      'reflectivity': 0.35,
      'clearcoat': 0.3,
      'clearcoatRoughness': 0.12,
      'transmission': 0.15
    });
  }

  group.openings.forEach(function(opening) {
    const is_glass_family = this.is_opening_glass_family_(opening.type);
    if (opening.type !== 'door' && is_glass_family !== true) {
      return;
    }

    const opening_line = this.get_opening_line_params_(opening);
    const width = opening_line.width;
    const height = Math.max(1, Number(opening.height || this.get_opening_default_height_(opening.type)));
    const center_z = this.get_opening_center_z_(group, opening, height);

    const fixture_group = new THREE.Group();
    fixture_group.position.set(
      opening_line.center_x,
      opening_line.center_y,
      center_z
    );
    fixture_group.rotation.z = opening_line.rotation_radians;

    const side_geometry = new THREE.BoxGeometry(frame_thickness, wall_thickness, height);
    const left_side = new THREE.Mesh(side_geometry, this.opening_frame_material_);
    left_side.position.x = -(width / 2) + (frame_thickness / 2);
    left_side.castShadow = true;
    left_side.receiveShadow = true;
    left_side.userData.is_opening = true;
    fixture_group.add(left_side);

    const right_side = new THREE.Mesh(side_geometry, this.opening_frame_material_);
    right_side.position.x = (width / 2) - (frame_thickness / 2);
    right_side.castShadow = true;
    right_side.receiveShadow = true;
    right_side.userData.is_opening = true;
    fixture_group.add(right_side);

    const top_geometry = new THREE.BoxGeometry(width, wall_thickness, frame_thickness);
    const top_frame = new THREE.Mesh(top_geometry, this.opening_frame_material_);
    top_frame.position.z = (height / 2) - (frame_thickness / 2);
    top_frame.castShadow = true;
    top_frame.receiveShadow = true;
    top_frame.userData.is_opening = true;
    fixture_group.add(top_frame);

    if (is_glass_family === true) {
      const bottom_frame = new THREE.Mesh(top_geometry, this.opening_frame_material_);
      bottom_frame.position.z = -(height / 2) + (frame_thickness / 2);
      bottom_frame.castShadow = true;
      bottom_frame.receiveShadow = true;
      bottom_frame.userData.is_opening = true;
      fixture_group.add(bottom_frame);

      // Keep pane nearly flush to the inner frame; only leave a tiny inset to
      // avoid edge z-fighting/flicker.
      const pane_inset = 0.05;
      const pane_width = Math.max(6, width - (frame_thickness * 2) - pane_inset);
      const pane_height = Math.max(6, height - (frame_thickness * 2) - pane_inset);
      const pane_depth = Math.max(0.25, wall_thickness * 0.18);
      const pane = new THREE.Mesh(
        new THREE.BoxGeometry(pane_width, pane_depth, pane_height),
        this.window_pane_material_
      );
      pane.castShadow = false;
      pane.receiveShadow = false;
      pane.userData.is_opening = true;
      fixture_group.add(pane);

      if (opening.type === 'window') {
        const divider_thickness = Math.max(1.4, frame_thickness * 0.7);
        const divider_overhang = Math.max(0.35, wall_thickness * 0.12);
        const divider_depth = wall_thickness + (divider_overhang * 2);
        const divider = new THREE.Mesh(
          new THREE.BoxGeometry(
            pane_width,
            divider_depth,
            divider_thickness
          ),
          this.opening_frame_material_
        );
        divider.position.z = 0;
        // Keep centered so the mullion protrudes equally from front/back faces.
        divider.position.y = 0;
        divider.castShadow = true;
        divider.receiveShadow = true;
        divider.userData.is_opening = true;
        fixture_group.add(divider);
      }
    } else if (opening.type === 'door') {
      const bottom_frame = new THREE.Mesh(top_geometry, this.opening_frame_material_);
      bottom_frame.position.z = -(height / 2) + (frame_thickness / 2);
      bottom_frame.castShadow = true;
      bottom_frame.receiveShadow = true;
      bottom_frame.userData.is_opening = true;
      fixture_group.add(bottom_frame);

      const door_clearance = 0.25;
      const door_width = Math.max(10, width - (frame_thickness * 2) - door_clearance);
      const door_height = Math.max(12, height - (frame_thickness * 2) - door_clearance);
      const door_depth = Math.max(0.6, wall_thickness * 0.35);
      const raw_door_color = String(opening.color || '#7a573b').toLowerCase();
      const door_color = ['#2d2d2d', '#3f3f3f'].includes(raw_door_color)
        ? '#4a4a4a'
        : raw_door_color;
      const door_material = new THREE.MeshStandardMaterial({
        'color': door_color,
        'roughness': 0.72,
        'metalness': 0.0
      });
      const door = new THREE.Mesh(
        new THREE.BoxGeometry(door_width, door_depth, door_height),
        door_material
      );
      door.position.z = 0;
      door.castShadow = true;
      door.receiveShadow = true;
      door.userData.is_opening = true;
      fixture_group.add(door);

    }

    layer.add(fixture_group);
  }, this);
};

