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 { query_edge_is_boundary } from "../query/query_edge_is_boundary.js";
import { query_edge_other_vertex } from "../query/query_edge_other_vertex.js";
import { query_vertex_in_edge } from "../query/query_vertex_in_edge.js";
import { query_vertices_in_edge } from "../query/query_vertices_in_edge.js";

let index_counter = 0;

export class TopoEdge {
    /**
     * Unique ID
     * @type {number}
     */
    index = index_counter++;

    /**
     *
     * @type {TopoVertex}
     */
    v0 = null;
    /**
     *
     * @type {TopoVertex}
     */
    v1 = null;


    /**
     *
     * @type {TopoTriangle[]}
     */
    faces = [];

    /**
     *
     * @type {number}
     */
    lengthSqr = -1;

    get byteSize() {
        return 80 + 5 * 4 + 4 + 8 + 8 + 8 + this.faces.length * 8 + 10;
    }

    /**
     *
     * @param {number} i
     * @returns {TopoVertex}
     */
    getVertexByIndex(i) {
        if (i === 0) {
            return this.v0;
        } else if (i === 1) {
            return this.v1;
        } else {
            throw new Error('Index out of bounds');
        }
    }

    /**
     *
     * @param {TopoEdge} other
     */
    copy(other) {
        this.v0 = other.v0;
        this.v1 = other.v1;

        this.lengthSqr = other.lengthSqr;

        this.faces = other.faces.slice();
    }

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

        r.copy(this);

        return r;
    }

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

        const faces = this.faces;
        const face_count = faces.length;

        // if (face_count > 2) {
        //     error_consumer(`Too many faces, a single edge can have at most two attached edge, instead got '${face_count}'`);
        //     valid = false;
        // }

        for (let i = 0; i < face_count; i++) {
            const face = faces[i];

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

        if (!this.v0.containsEdge(this)) {
            error_consumer(`Missing back-link from vertex v0`);
            valid = false;
        }

        if (!this.v1.containsEdge(this)) {
            error_consumer(`Missing back-link from vertex v1`);
            valid = false;
        }

        return valid;
    }

    /**
     * Returns false if edge is not licked back to by it's vertices (v0 is used for the check)
     * @return {boolean}
     */
    isLinked() {
        return this.v0.containsEdge(this);
    }

    /**
     * Remove self from the topology graph
     */
    unlink() {
        const v0 = this.v0;
        const v1 = this.v1;

        v0.removeEdge(this);

        if (v0 !== v1) {
            // special check for degenerate edges where both vertices are the same
            v1.removeEdge(this);
        }

        const faces = this.faces;
        const face_count = faces.length;
        for (let i = 0; i < face_count; i++) {
            const face = faces[i];

            face.removeEdge(this);
        }
    }

    /**
     *
     * @returns {boolean}
     */
    isDegenerateEdge() {
        return this.v0 === this.v1;
    }

    /**
     * @deprecated use {@link query_edge_is_boundary} instead
     * Is this a an edge of topology as a whole?
     * @return {boolean}
     */
    isTopologyBorder() {
        throw new Error(`deprecated, use "query_edge_is_boundary" instead`)
    }

    /**
     * Note: requires length square to be pre-computed
     * @return {number}
     */
    get length() {
        if (this.lengthSqr < 0) {
            this.computeSquaredLength();
        }

        return Math.sqrt(this.lengthSqr);
    }

    computeSquaredLength() {
        const v0 = this.v0;
        const v1 = this.v1;

        const x0 = v0.x;
        const y0 = v0.y;
        const z0 = v0.z;

        const x1 = v1.x;
        const y1 = v1.y;
        const z1 = v1.z;

        const dx = x0 - x1;
        const dy = y0 - y1;
        const dz = z0 - z1;

        this.lengthSqr = dx * dx + dy * dy + dz * dz;
    }

    /**
     * NOTE: does not update faces
     * @param {TopoVertex} existing
     * @param {TopoVertex} replacement
     */
    replaceVertex(existing, replacement) {
        if (existing === this.v0) {
            this.v0 = replacement;
        }

        // NOTE: we're supporting case where both vertices are the same (degenerate edge)
        if (existing === this.v1) {
            this.v1 = replacement;
        }
    }

    /**
     * Absorb other edge, as a result other edge becomes redundant
     * NOTE: edges MUST share vertices
     * @param {TopoEdge} other
     */
    merge(other) {
        assert.equal(other.isTopoEdge, true, "other.isTopoEdge !== true");
        assert.notEqual(this, other, "other === this");

        //absorb faces
        const other_faces = other.faces;

        const own_faces = this.faces;

        const face_count = other_faces.length;
        for (let i = 0; i < face_count; i++) {
            const face = other_faces[i];

            //remove references to old edge from the face
            face.removeEdge(other);

            if (own_faces.indexOf(face) === -1) {
                // not patched yet
                own_faces.push(face);

                //add this edge to face
                face.addUniqueEdge(this);
            }

        }

        //patch vertices
        const other_v0 = other.v0;

        assert.equal(this.v0 === other_v0 || this.v1 === other_v0, true, 'this edge does not include other.v0');

        //cut old edge from vertex 0
        other_v0.removeEdge(other);

        const other_v1 = other.v1;

        assert.equal(this.v0 === other_v1 || this.v1 === other_v1, true, 'this edge does not include other.v1');

        //cut old edge from vertex 1
        other_v1.removeEdge(other);
    }

    /**
     *
     * @param {TopoTriangle} face
     * @return {boolean}
     */
    containsFace(face) {
        return this.faces.indexOf(face) !== -1;
    }

    /**
     *
     * @param {TopoTriangle} face
     */
    addFace(face) {
        assert.equal(face.isTopoFace, true, "face.isTopoFace !== true");
        assert.arrayHasNo(this.faces, face, 'already contains this face');

        this.faces.push(face);
    }

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

        array_push_if_unique(this.faces, face);
    }

    /**
     *
     * @param {TopoTriangle} f
     */
    removeFace(f) {
        array_remove_first(this.faces, f);

        assert.arrayHasNo(this.faces, f, 'found an instance of removed face');
    }

    /**
     * @deprecated use {@link query_edge_other_vertex} instead
     * @param {TopoVertex} v
     * @return {TopoVertex}
     */
    getOtherVertex(v) {
        return query_edge_other_vertex(this, v);
    }

    /**
     * @deprecated use {@link @query_vertex_in_edge} instead
     * @param {TopoVertex} v
     * @return {boolean}
     */
    containsVertex(v) {
        return query_vertex_in_edge(this, v);
    }

    /**
     * @deprecated use {@link query_vertices_in_edge} instead
     * @param {TopoVertex} a
     * @param {TopoVertex} b
     * @return {boolean}
     */
    containsBothVertices(a, b) {
        return query_vertices_in_edge(this, a, b);
    }

    /**
     *
     * @param {TopoEdge} other
     * @returns {boolean}
     */
    containsSameVerticesAs(other) {
        return query_vertices_in_edge(this, other.v0, other.v1);
    }
}

/**
 * @readonly
 * @type {boolean}
 */
TopoEdge.prototype.isTopoEdge = true;
