import { vec3 } from "gl-matrix";
import { assert } from "../../../../assert.js";
import { array_push_if_unique } from "../../../../collection/array/array_push_if_unique.js";
import { array_remove_first } from "../../../../collection/array/array_remove_first.js";
import { array_replace_all } from "../../../../collection/array/array_replace_all.js";
import { compute_triangle_normal } from "../../triangle/compute_triangle_normal.js";
import { TopoEdge } from "./TopoEdge.js";
import { TopoVertex } from "./TopoVertex.js";

let index_counter = 0;

export class TopoTriangle {
    /**
     * Identifying index, can be used to link back to buffer-geometry face by index or any other useful ID purposes
     * @type {number}
     */
    index = index_counter++;
    /**
     *
     * @type {TopoVertex[]}
     */
    vertices = [];
    /**
     *
     * @type {TopoEdge[]}
     */
    edges = [];

    /**
     *
     * @type {number[]|vec3}
     */
    normal = [0, 0, 0];

    get byteSize() {
        return 80 + 4 * 4 + 4 + this.vertices.length * 8 + 10 + this.edges.length * 8 + 10 + 8 * 3 + 10;
    }

    /**
     *
     * @param {TopoTriangle} other
     */
    copy(other) {
        this.index = other.index;
        this.vertices = other.vertices.slice();
        this.edges = other.edges.slice();

        vec3.copy(this.normal, other.normal);
    }

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

        r.copy(this);

        return r;
    }

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

        const vertices = this.vertices;
        const vertex_count = vertices.length;

        if (vertex_count !== 3) {
            error_consumer(`Expected number of vertices is 3, instead got ${vertex_count}`);
            valid = false;
        }

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

            if (!vertex.containsFace(this)) {
                error_consumer(`Missing back-link from vertex[${i}]`);
                valid = false;
            }
        }

        const edges = this.edges;
        const edge_count = edges.length;
        if (edge_count !== 3) {
            error_consumer(`Expected number of edges is 3, instead got ${edge_count}`);
            valid = false;
        }

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

            if (!edge.containsFace(this)) {
                error_consumer(`Missing back-link from edge[${i}]`);
                valid = false;
            }
        }

        //TODO check uniqueness of vertices
        //TODO check uniqueness of edges

        return valid;
    }

    /**
     *
     * @return {boolean}
     */
    isLinked() {
        const v0 = this.vertices[0];
        return v0.containsFace(this);
    }

    /**
     * NOTE: does not update vertices
     * @param {TopoEdge} existing
     * @param {TopoEdge} replacement
     */
    replaceEdge(existing, replacement) {
        array_replace_all(this.edges, existing, replacement);
    }

    /**
     * NOTE: does not update edges
     * @param {TopoVertex} existing
     * @param {TopoVertex} replacement
     */
    replaceVertex(existing, replacement) {
        array_replace_all(this.vertices, existing, replacement);

        assert.arrayHasNo(this.vertices, existing, 'found removed element');
    }

    /**
     * Some or all vertices are the same, resulting in line or point representation
     */
    isDegenerateTopology() {

        const vertices = this.vertices;

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

        return vA === vB || vA === vC || vB === vC;
    }

    /**
     * Remove self from the topology graph
     */
    unlink() {
        // update references of vertices
        const vertices = this.vertices;
        const vertex_count = vertices.length;

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

            vertex.removeFace(this);
        }

        // update references of edges
        const edges = this.edges;
        const edge_count = edges.length;
        for (let i = 0; i < edge_count; i++) {
            const edge = edges[i];

            edge.removeFace(this);
        }
    }

    /**
     *
     * @param {TopoEdge} e
     */
    addEdge(e) {
        assert.arrayHasNo(this.edges, e, 'already contains this edge');

        this.edges.push(e);
    }

    /**
     *
     * @param {TopoEdge} e
     * @return {boolean}
     */
    addUniqueEdge(e) {
        return array_push_if_unique(this.edges, e);
    }

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

        array_remove_first(this.edges, e);

        assert.arrayHasNo(this.edges, e, 'contains removed edge');
    }

    /**
     *
     * @param {TopoEdge} edge
     * @returns {boolean}
     */
    containsEdge(edge) {
        assert.equal(edge.isTopoEdge, true, 'edge.isTopoEdge !== true');
        return this.edges.indexOf(edge) !== -1;
    }

    computeNormal() {
        const vertices = this.vertices;

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

        compute_triangle_normal(this.normal, vA, vB, vC);


        // if (this.normal.z === -1) {
        //     debugger;
        // }
    }


    /**
     *
     * @param {number} index
     * @param {TopoVertex} vertex
     */
    setVertexAt(index, vertex) {
        assert.equal(vertex.isTopoVertex, true, 'vertex.isTopoVertex !== true');
        assert.isNonNegativeInteger(index, 'index');

        this.vertices[index] = vertex;
    }

    /**
     *
     * @param {TopoVertex} vertex
     * @return {boolean}
     */
    containsVertex(vertex) {
        return this.vertices.indexOf(vertex) !== -1;
    }

    /**
     * Get all neighbours that share an edge with this face
     * @param {TopoTriangle[]} result
     * @param {number} result_offset
     * @returns {number} number of found neighbours
     */
    getEdgeNeighbours(result, result_offset) {
        let additions = 0;

        const edges = this.edges;
        const edge_count = edges.length;

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

            const edge_faces = edge.faces;

            const edge_face_count = edge_faces.length;

            for (let j = 0; j < edge_face_count; j++) {
                const f = edge_faces[j];

                if (f === this) {
                    continue;
                }

                // NOTE: this is already de-duped, as edges only have 2 neighbours, and this face is one of the two
                result[result_offset + additions] = f;

                additions++;
            }
        }

        return additions;
    }

    /**
     * Utility constructor
     * @param {number} ax
     * @param {number} ay
     * @param {number} az
     * @param {number} bx
     * @param {number} by
     * @param {number} bz
     * @param {number} cx
     * @param {number} cy
     * @param {number} cz
     * @return {TopoTriangle}
     */
    static fromPoints(
        ax, ay, az,
        bx, by, bz,
        cx, cy, cz
    ) {

        const v0 = TopoVertex.from(ax, ay, az);
        const v1 = TopoVertex.from(bx, by, bz);
        const v2 = TopoVertex.from(cx, cy, cz);

        const e0 = new TopoEdge();
        const e1 = new TopoEdge();
        const e2 = new TopoEdge();

        e0.v0 = v0;
        e0.v1 = v1;

        e1.v0 = v1;
        e1.v1 = v2;

        e2.v0 = v2;
        e2.v1 = v0;

        const triangle = new TopoTriangle();

        triangle.edges.push(e0, e1, e2);
        triangle.vertices.push(v0, v1, v2);

        // link
        v0.faces.push(triangle);
        v1.faces.push(triangle);
        v2.faces.push(triangle);

        e0.faces.push(triangle);
        e1.faces.push(triangle);
        e2.faces.push(triangle);

        v0.edges.push(e0, e2);
        v1.edges.push(e0, e1);
        v2.edges.push(e1, e2);

        return triangle;
    }
}

/**
 * @readonly
 * @type {boolean}
 */
TopoTriangle.prototype.isTopoFace = true;
