import { assert } from "../../../../core/assert.js";
import { BVH } from "../../../../core/bvh2/bvh3/BVH.js";
import { BinaryTopology } from "../../../../core/geom/3d/topology/struct/binary/BinaryTopology.js";
import {
    bt_mesh_cleanup_faceless_references
} from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_cleanup_faceless_references.js";
import {
    bt_mesh_compact
} from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_compact.js";
import {
    bt_mesh_compute_face_normals
} from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_compute_face_normals.js";

import {
    bt_mesh_face_decouple_islands
} from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_face_decouple_islands.js";
import {
    bt_mesh_face_island_erode
} from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_face_island_erode.js";
import {
    bt_mesh_from_unindexed_geometry
} from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_from_unindexed_geometry.js";
import { bt_mesh_triangulate } from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_triangulate.js";
import {
    bt_mesh_fuse_duplicate_edges
} from "../../../../core/geom/3d/topology/struct/binary/io/edge/bt_mesh_fuse_duplicate_edges.js";
import {
    bt_merge_verts_by_distance
} from "../../../../core/geom/3d/topology/struct/binary/io/vertex/bt_merge_verts_by_distance.js";
import { bt_face_is_degenerate } from "../../../../core/geom/3d/topology/struct/binary/query/bt_face_is_degenerate.js";
import {
    bt_mesh_bridge_islands
} from "../../../../core/geom/3d/topology/struct/binary/io/bt_mesh_bridge_islands.js";
import {
    bt_mesh_compute_face_islands
} from "../../../../core/geom/3d/topology/struct/binary/query/bt_mesh_compute_face_islands.js";
import { v3_angle_between } from "../../../../core/geom/vec3/v3_angle_between.js";
import Vector3 from "../../../../core/geom/Vector3.js";
import { bvh_build_from_bt_mesh } from "../bvh_build_from_bt_mesh.js";
import { bvh_build_from_unindexed_triangles } from "./bvh_build_from_unindexed_triangles.js";
import { enforce_agent_height_clearance } from "./enforce_agent_height_clearance.js";


/**
 * Build from given scene geometry
 * @param {BinaryTopology} destination
 * @param {BinaryTopology} source Must be triangulated. Use {@link bt_mesh_triangulate} if needed.
 * @param {number} [agent_radius]
 * @param {number} [agent_height]
 * @param {number} [agent_max_step_height] agent can bridge vertical gaps in topology, such as stepping up a stair
 * @param {number} [agent_max_step_distance] agent can bridge lateral gaps in topology, such as stepping over a hole in the floor
 * @param {number} [agent_max_climb_angle] In radians, how steep of an angle can the agent go up by
 * @param {Vector3} [up] Defines world's "UP" direction, this is what the agent will respect for climbing constraint
 */
export function navmesh_build_topology({
                                           destination,
                                           source,
                                           agent_radius = 0,
                                           agent_height = 2,
                                           agent_max_step_height = 0,
                                           agent_max_step_distance = 0,
                                           agent_max_climb_angle = Math.PI / 4,
                                           up = Vector3.up,
                                       }) {

    assert.defined(destination, 'destination');
    assert.defined(source, 'source');

    assert.greaterThanOrEqual(agent_radius, 0, 'agent_radius');
    assert.greaterThanOrEqual(agent_height, 0, 'agent_height');
    assert.greaterThanOrEqual(agent_max_climb_angle, 0, 'agent_max_climb_angle');
    assert.greaterThanOrEqual(agent_max_step_height, 0, 'agent_max_step_height');
    assert.greaterThanOrEqual(agent_max_step_distance, 0, 'agent_max_step_distance');

    // The walkable-angle filter below reads each source face's normal. Compute them defensively rather
    // than relying on an undocumented "caller already populated normals" precondition - normals are
    // derived data, so this is idempotent. NOTE: a single face normal cannot distinguish a correctly
    // wound floor from a flipped one (or a ceiling), so the source is still required to have consistent
    // outward winding; that remains an input contract we cannot recover from here.
    bt_mesh_compute_face_normals(source);

    const source_bvh = new BVH();

    // prepare a BVH, we'll need it for height queries later on
    bvh_build_from_bt_mesh(source_bvh, source);

    const scratch_normal = new Float32Array(3);

    // unpack topology into triangle soup
    const source_face_count = source.faces.size;

    // TODO add cleanup phase
    //      - fuse vertices of tiny triangles eliminating them
    //      - identify flat areas and re-triangulate them to remove unnecessary triangles

    /**
     *
     * @type {number[]}
     */
    const raw_triangles = [];
    let triangle_count = 0;

    for (let face_id = 0; face_id < source_face_count; face_id++) {

        source.face_read_normal(scratch_normal, 0, face_id);

        const angle = v3_angle_between(up.x, up.y, up.z, scratch_normal[0], scratch_normal[1], scratch_normal[2]);

        if (angle > agent_max_climb_angle) {
            // discard
            continue;
        }

        if (bt_face_is_degenerate(source, face_id)) {
            // discard
            continue;
        }

        let loop = source.face_read_loop(face_id);
        const loop_start = loop;

        let face_vertex_count = 0;

        do {

            assert.lessThan(face_vertex_count, 3, 'not a triangle');

            const vertex = source.loop_read_vertex(loop);

            const triangle_offset = triangle_count * 9;

            source.vertex_read_coordinate(raw_triangles, triangle_offset + face_vertex_count * 3, vertex);

            face_vertex_count++;

            loop = source.loop_read_next(loop);

        } while (loop !== loop_start)

        triangle_count++;
    }

    // knock out holes in triangles based on agent's height
    if (agent_height > 0) {

        triangle_count = enforce_agent_height_clearance({
            bvh: source_bvh,
            source: source,
            agent_height: agent_height,
            agent_radius: agent_radius,
            triangle_count: triangle_count,
            triangles: raw_triangles,
            up: up,
        });

    }

    {
        const bvh = new BVH();

        // first dump all triangles into a BVH for speed of access
        bvh_build_from_unindexed_triangles(bvh, raw_triangles, triangle_count);

        // find possible triangle edge connections, that is - where edges of 2 triangles touch, but don't match exactly.
        // the touching edges will need to be cut and affected triangles split accordingly

        const mesh = new BinaryTopology();

        // raw_triangles may have stale tail data from filtering steps above (e.g. height clearance);
        // bt_mesh_from_unindexed_geometry reads the whole array, so trim to the live range first.
        raw_triangles.length = triangle_count * 9;

        bt_mesh_from_unindexed_geometry(mesh, raw_triangles);

        // fuse coincident positions so neighbouring triangles share a vertex id...
        bt_merge_verts_by_distance(mesh, 1e-6);

        // ...then fuse the resulting duplicate edges so they share an edge id too,
        // which is what the loop-based neighbour queries rely on
        bt_mesh_fuse_duplicate_edges(mesh);

        const mesh_bounds = new Float32Array(6);
        bvh.node_get_aabb(bvh.root, mesh_bounds);

        const islands = bt_mesh_compute_face_islands(mesh);

        // enabled us to modify islands independently
        bt_mesh_face_decouple_islands(mesh, islands);

        // shrink islands to agent_radius
        for (const island of islands) {
            bt_mesh_face_island_erode(mesh, island, agent_radius);
        }

        // TODO attempt to reduce mesh complexity by re-triangulating flat areas or running a very constrained decimation

        // remove dangling references
        bt_mesh_cleanup_faceless_references(mesh);

        // Island erosion (above) kills faces via the pool's free-list, leaving holes in the ID space.
        // `faces.size` is a high-water mark, not a live count, so consumers that iterate `0..faces.size`
        // (e.g. bvh_build_from_bt_mesh) would touch freed slots. Compact so every consumer sees a dense,
        // hole-free topology - this is the single invariant downstream code relies on.
        bt_mesh_compact(mesh);

        // bridge across small steps / gaps so stair-separated or slightly-broken tiers are reachable.
        // Opt-in: with both step params 0 (the default) the topology is left untouched.
        if (agent_max_step_height > 0 || agent_max_step_distance > 0) {
            bt_mesh_bridge_islands(mesh, {
                max_step_height: agent_max_step_height,
                max_step_distance: agent_max_step_distance,
                up,
            });

            // bridging rebuilds via vertex-merge / edge-fuse, which can leave holes; re-densify
            bt_mesh_compact(mesh);
        }

        // face normals are consumed by navigation queries (string-pulling portal normals), populate them now
        bt_mesh_compute_face_normals(mesh);

        // write out to destination
        destination.copy(mesh);
    }

}