import { vec3 } from "gl-matrix";
import { assert } from "../../../../assert.js";
import { array_push_if_unique } from "../../../../collection/array/array_push_if_unique.js";
import { noop } from "../../../../function/noop.js";

import { number_format_by_thousands } from "../../../../primitives/numbers/number_format_by_thousands.js";
import { query_edge_other_vertex } from "../query/query_edge_other_vertex.js";
import { tm_kill_only_edge } from "../tm_kill_only_edge.js";
import { tm_kill_only_face } from "../tm_kill_only_face.js";
import { tm_kill_only_vert } from "../tm_kill_only_vert.js";
import { TopoEdge } from "./TopoEdge.js";
import { TopoTriangle } from "./TopoTriangle.js";
import { TopoVertex } from "./TopoVertex.js";

/**
 * Triangulated mesh graph.
 * @see BinaryTopology
 */
export class TopoMesh {

    /**
     *
     * @type {TopoVertex[]}
     */
    vertices = [];

    /**
     *
     * @type {Set<TopoEdge>}
     * @private
     */
    __edges = new Set();


    /**
     *
     * @type {Set<TopoTriangle>}
     * @private
     */
    __faces = new Set();

    /**
     * Approximation of memory footprint of this object
     * NOTE: this is highly speculative and will differ from reality depending on VM running the code as well as many other factors
     * @return {number}
     */
    get byteSize() {
        let r = 0;

        for (let i = 0; i < this.vertices.length; i++) {
            const v = this.vertices[i];

            r += v.byteSize;
        }

        for (const edge of this.__edges) {
            r += edge.byteSize;
        }

        for (const face of this.__faces) {
            r += face.byteSize;
        }

        return r;
    }

    /**
     *
     * @returns {Set<TopoEdge>}
     */
    getEdges() {
        return this.__edges;
    }

    /**
     *
     * @returns {Set<TopoTriangle>}
     */
    getFaces() {
        return this.__faces;
    }

    /**
     *
     * @param {number} index
     * @return {undefined|TopoTriangle}
     */
    getFaceByIndex(index) {
        assert.isNonNegativeInteger(index, 'index');

        for (const face of this.__faces) {
            if (face.index === index) {
                return face;
            }
        }

        return undefined;
    }

    /**
     *
     * @param {function(reason:string)} [error_consumer]
     * @returns {boolean}
     */
    validate(error_consumer = noop) {
        let valid = true;

        let error_context = "";
        let error_index = 0;

        function consume_sub_error(reason) {
            error_consumer(`${error_context}[${error_index}]: ${reason}`);
            valid = false;
        }

        error_context = "Edge";

        error_index = 0;

        const edges = this.getEdges();
        for (let edge of edges) {

            edge.validate(consume_sub_error);

            if (!this.containsVertex(edge.v0)) {
                consume_sub_error(`Link to off-mesh vertex v0`);
            }

            if (!this.containsVertex(edge.v1)) {
                consume_sub_error(`Link to off-mesh vertex v1`);
            }


            const edge_faces = edge.faces;
            const edge_face_count = edge_faces.length;
            for (let j = 0; j < edge_face_count; j++) {
                const edge_face = edge_faces[j];
                if (!this.containsFace(edge_face)) {
                    consume_sub_error(`Link to off-mesh face[${j}]`);
                }
            }

            error_index++;
        }


        const faces = this.getFaces();

        error_context = "Face";

        error_index = 0;
        for (let face of faces) {

            face.validate(consume_sub_error);

            const face_edges = face.edges;
            const face_edge_count = face_edges.length;
            for (let j = 0; j < face_edge_count; j++) {
                const face_edge = face_edges[j];

                if (!this.containsEdge(face_edge)) {
                    consume_sub_error(`Link to off-mesh edge[${j}]`);
                }
            }

            const face_vertices = face.vertices;
            const face_vertex_count = face_vertices.length;
            for (let j = 0; j < face_vertex_count; j++) {
                const face_vertex = face_vertices[j];
                if (!this.containsVertex(face_vertex)) {
                    consume_sub_error(`Link to off-mesh vertex[${j}]`);
                }
            }
            error_index++;
        }
        const vertices = this.vertices;
        const vertex_count = vertices.length;

        error_context = "Vertex";

        for (let i = 0; i < vertex_count; i++) {
            error_index = i;

            const vertex = vertices[i];

            vertex.validate(consume_sub_error);

            const vertex_edges = vertex.edges;
            const vertex_edge_count = vertex_edges.length;

            for (let j = 0; j < vertex_edge_count; j++) {
                const vertex_edge = vertex_edges[j];

                if (!this.containsEdge(vertex_edge)) {
                    consume_sub_error(`Link to off-mesh edge[${j}]`);
                }
            }

            const vertex_faces = vertex.faces;
            const vertex_face_count = vertex_faces.length;
            for (let j = 0; j < vertex_face_count; j++) {
                const vertex_face = vertex_faces[j];
                if (!this.containsFace(vertex_face)) {
                    consume_sub_error(`Link to off-mesh face[${j}]`);
                }
            }
        }
        return valid;
    }

    clone() {
        const r = new TopoMesh();

        r.add(this);

        return r;
    }

    /**
     *
     * @returns {Map<number, TopoVertex>}
     */
    buildVertexMapping() {

        /**
         * map vertices by index
         * @type {Map<number, TopoVertex>}
         */
        const vertex_map = new Map();

        const this_vertices = this.vertices;
        const this_vertex_count = this_vertices.length;

        for (let i = 0; i < this_vertex_count; i++) {
            const vertex = this_vertices[i];

            vertex_map.set(vertex.index, vertex);
        }

        return vertex_map;
    }

    /**
     *
     * @param {TopoMesh} other
     * @param {Map<number, TopoVertex>} vertex_map
     */
    _addWithVertexMap(other, vertex_map) {
        assert.notEqual(this, other, "can't add self");

        // Ingest faces
        const other_faces = other.getFaces();

        for (let face of other_faces) {

            this.addFaceCopy(face, vertex_map);
        }
    }

    /**
     * Will create a topologically matching copy of the given face in this Mesh
     * @param {TopoTriangle} face
     * @param {Map<number,TopoVertex>} vertex_map
     */
    addFaceCopy(face, vertex_map) {

        const f = new TopoTriangle();

        // copy face normal
        vec3.copy(f.normal, face.normal);

        const face_vertices = face.vertices;

        for (let j = 0; j < 3; j++) {
            const other_vertex = face_vertices[j];

            let v = vertex_map.get(other_vertex.index);

            if (v === undefined) {
                v = new TopoVertex();
                v.index = other_vertex.index;

                v.x = other_vertex.x;
                v.y = other_vertex.y;
                v.z = other_vertex.z;

                vertex_map.set(other_vertex.index, v);

                this.addVertex(v);
            }

            f.setVertexAt(j, v);

            v.addUniqueFace(f);

        }

        this.addFace(f);
        this.patchFaceEdges(f);
    }

    /**
     * Ensures that face has correct edges.
     * NOTE: this method is additive, it will not *NOT* remove any references
     * @param {TopoTriangle} face
     */
    patchFaceEdges(face) {

        const f_vertices = face.vertices;

        const vA = f_vertices[0];
        const vB = f_vertices[1];
        const vC = f_vertices[2];

        // patch edges
        const edge0 = this.ensureEdge(vA, vB);
        const edge1 = this.ensureEdge(vB, vC);
        const edge2 = this.ensureEdge(vC, vA);

        edge0.addUniqueFace(face);
        edge1.addUniqueFace(face);
        edge2.addUniqueFace(face);

        face.addUniqueEdge(edge0);
        face.addUniqueEdge(edge1);
        face.addUniqueEdge(edge2);
    }

    /**
     *
     * @param {TopoMesh} other
     */
    add(other) {
        assert.notEqual(this, other, "can't add self");

        /**
         * map vertices by index
         * @type {Map<number, TopoVertex>}
         */
        const vertex_map = this.buildVertexMapping();


        this._addWithVertexMap(other, vertex_map);

    }

    /**
     *
     * @param {TopoVertex} v
     */
    addVertex(v) {
        assert.equal(v.isTopoVertex, true, "v.isTopoVertex !== true");
        this.vertices.push(v);
    }

    /**
     *
     * @param {TopoVertex} v
     */
    addUniqueVertex(v) {
        assert.equal(v.isTopoVertex, true, "v.isTopoVertex !== true");

        array_push_if_unique(this.vertices, v);
    }

    /**
     * @deprecated use {@link tm_kill_only_vert}
     * @param {TopoVertex} v
     */
    removeVertex(v) {
        throw new Error(`deprecated, use tm_kill_only_vert instead`)
    }

    /**
     *
     * @param {TopoVertex} v
     * @returns {boolean}
     */
    containsVertex(v) {
        assert.equal(v.isTopoVertex, true, "v.isTopoVertex !== true");
        return this.vertices.indexOf(v) !== -1;
    }


    /**
     *
     * @param {TopoEdge} e
     */
    addEdge(e) {
        assert.equal(e.isTopoEdge, true, "e.isTopoEdge !== true");
        this.__edges.add(e);
    }

    /**
     *
     * @param {TopoEdge} e
     */
    addUniqueEdge(e) {
        assert.equal(e.isTopoEdge, true, "e.isTopoEdge !== true");

        this.__edges.add(e);
    }

    /**
     * @deprecated use {@link tm_kill_only_edge}
     * @param {TopoEdge} e
     */
    removeEdge(e) {
        throw new Error(`deprecated, use tm_kill_only_edge instead`)
    }

    /**
     *
     * @param {TopoEdge} e
     * @returns {boolean}
     */
    containsEdge(e) {
        assert.equal(e.isTopoEdge, true, "e.isTopoEdge !== true");
        return this.__edges.has(e);
    }

    /**
     *
     * @param {TopoTriangle} f
     */
    addFace(f) {
        assert.equal(f.isTopoFace, true, "f.isTopoFace !== true");

        // assert.arrayHasNo(this.faces, f, 'already contains this face');

        // this.faces.push(f);

        this.__faces.add(f);
    }

    /**
     *
     * @param {Iterable<TopoTriangle>} faces
     */
    injectManyFaces(faces) {
        for (let face of faces) {
            this.injectFace(face);
        }
    }

    /**
     * Add face as well as it's edges and vertices
     * @param {TopoTriangle} f
     */
    injectFace(f) {
        assert.equal(f.isTopoFace, true, "f.isTopoFace !== true");
        this.addFace(f);

        const edges = f.edges;

        const edge_count = edges.length;

        for (let i = 0; i < edge_count; i++) {
            const edge = edges[i];

            this.addUniqueEdge(edge);
        }

        const vertices = f.vertices;
        const vertex_count = vertices.length;

        for (let i = 0; i < vertex_count; i++) {
            const vertex = vertices[i];

            this.addUniqueVertex(vertex);
        }
    }

    /**
     * @deprecated use {@link tm_kill_only_face} instead
     * @param {TopoTriangle} f
     */
    removeFace(f) {
        throw new Error(`deprecated, use tm_kill_only_face instead`)
    }

    /**
     *
     * @param {TopoTriangle} f
     * @returns {boolean}
     */
    containsFace(f) {
        assert.equal(f.isTopoFace, true, "f.isTopoFace !== true");
        return this.__faces.has(f);
    }

    /**
     * Creates an edge between two vertices, or if one already exists - returns that
     * @param {TopoVertex} a
     * @param {TopoVertex} b
     *
     * @returns {TopoEdge} Edge connecting vertices A and B
     */
    ensureEdge(a, b) {
        assert.defined(a, 'a');
        assert.defined(b, 'b');

        assert.equal(a.isTopoVertex, true, 'a.isTopoVertex !== true');
        assert.equal(b.isTopoVertex, true, 'b.isTopoVertex !== true');

        // DEBUG degenerate edge check
        // assert.notEqual(a, b, 'a === b, attempting to create an edge from vertex to itself');

        const aEdges = a.edges;
        const n = aEdges.length;

        for (let i = 0; i < n; i++) {
            const edge = aEdges[i];

            if (query_edge_other_vertex(edge, a) === b) {
                // found existing edge between A and B
                return edge;
            }
        }

        //edge doesn't exist, lets create one
        const topoEdge = new TopoEdge();

        topoEdge.v0 = a;
        topoEdge.v1 = b;

        a.edges.push(topoEdge);
        b.edges.push(topoEdge);

        this.__edges.add(topoEdge);

        return topoEdge;
    }

    mergeEdges() {
        const edges = this.getEdges();
        for (let e0 of edges) {

            const e0v0 = e0.v0;
            const e0v1 = e0.v1;

            for (let e1 of edges) {

                if (e0 === e1) {
                    continue;
                }

                const e1v0 = e1.v0;
                const e1v1 = e1.v1;

                if (e0v0 === e1v0) {
                    if (e0v1 !== e1v1) {
                        //not a match
                        continue;
                    }
                } else if (e0v0 === e1v1) {
                    if (e0v1 !== e1v0) {
                        //not a match
                        continue;
                    }
                } else {
                    //not a match
                    continue;
                }

                //cut the second edge
                tm_kill_only_edge(this, e1);

                //absorb edge
                e0.merge(e1);
            }
        }
    }

    computeEdgeSquaredLengths() {
        const edges = this.getEdges();

        for (let edge of edges) {

            edge.computeSquaredLength();

        }
    }

    /**
     *
     * @param {Float32Array} vertices
     */
    setVerticesFromArray(vertices) {
        assert.defined(vertices, 'vertices');

        const vertex_array_size = vertices.length;

        assert.equal(vertex_array_size % 3, 0, `Vertex array size must be multiple of 3, instead was ${vertex_array_size}`)

        const vertex_count = vertex_array_size / 3;

        //populate vertices
        const topo_vertices = this.vertices;

        for (let i = 0; i < vertex_count; i++) {

            const i3 = i * 3;

            const v = new TopoVertex();

            v.index = i;

            v.x = vertices[i3];
            v.y = vertices[i3 + 1];
            v.z = vertices[i3 + 2];

            topo_vertices[i] = v;
        }

    }

    /**
     *
     * @param {Uint16Array|Uint32Array|number[]} faces
     */
    setFacesFromIndexArray(faces) {
        const face_array_size = faces.length;

        assert.equal(face_array_size % 3, 0, `Face array size must be multiple of 3, instead was ${face_array_size}`)

        //populate vertices
        const topo_vertices = this.vertices;

        const face_count = face_array_size / 3;

        // populate faces

        for (let i = 0; i < face_count; i++) {
            const i3 = i * 3;

            const a = faces[i3];
            const b = faces[i3 + 1];
            const c = faces[i3 + 2];

            this.createTriangle(
                i,
                topo_vertices[a],
                topo_vertices[b],
                topo_vertices[c]
            );
        }
    }

    setFacedUnindexed() {
        const topo_vertices = this.vertices;

        const face_count = topo_vertices.length / 3;

        // populate faces
        for (let i = 0; i < face_count; i++) {

            const offset = i * 3;

            this.createTriangle(
                i,
                topo_vertices[offset],
                topo_vertices[offset + 1],
                topo_vertices[offset + 2]
            );
        }
    }

    /**
     *
     * @param {Float64Array|Float32Array|number[]} vertices
     * @param {Uint32Array|Uint16Array|Uint8Array|number[]} [faces]
     */
    build(vertices, faces) {

        this.setVerticesFromArray(vertices);

        if (faces === undefined || faces === null) {
            this.setFacedUnindexed();
        } else {
            this.setFacesFromIndexArray(faces);
        }

    }

    /**
     *
     * @param {number} index
     * @param {TopoVertex} v0
     * @param {TopoVertex} v1
     * @param {TopoVertex} v2
     * @returns {TopoTriangle}
     */
    createTriangle(index, v0, v1, v2) {
        const f = new TopoTriangle();

        f.index = index;

        const e01 = this.ensureEdge(v0, v1);
        const e12 = this.ensureEdge(v1, v2);
        const e20 = this.ensureEdge(v2, v0);

        //link primitives
        e01.faces.push(f);
        e12.faces.push(f);
        e20.faces.push(f);

        f.vertices.push(v0, v1, v2);
        f.edges.push(e01, e12, e20);

        v0.faces.push(f);
        v1.faces.push(f);
        v2.faces.push(f);

        this.__faces.add(f);

        return f;
    }

    toString() {
        return `TopoMesh{ vertices: ${number_format_by_thousands(this.vertices.length)}, edges: ${number_format_by_thousands(this.getEdges().size)}, faces: ${number_format_by_thousands(this.getFaces().size)} }`;
    }
}


/**
 * @readonly
 * @type {boolean}
 */
TopoMesh.prototype.isTopoMesh = true;
