import { orient3d } from "robust-predicates";
import { assert } from "../../../assert.js";
import { Base64 } from "../../../binary/base64/Base64.js";
import { BinaryBuffer } from "../../../binary/BinaryBuffer.js";
import { EndianType } from "../../../binary/EndianType.js";
import { array_copy } from "../../../collection/array/array_copy.js";
import { array_quick_sort_by_comparator } from "../../../collection/array/array_quick_sort_by_comparator.js";
import { typed_array_copy } from "../../../collection/array/typed/typed_array_copy.js";
import { max3 } from "../../../math/max3.js";
import { number_compare_descending } from "../../../primitives/numbers/number_compare_descending.js";

/**
 * @readonly
 * @type {number}
 */
const LAYOUT_TETRA_WORD_COUNT = 8;

/**
 * Size in bytes of a single tetrahedron record
 * @readonly
 * @type {number}
 */
export const LAYOUT_TETRA_BYTE_SIZE = LAYOUT_TETRA_WORD_COUNT * 4;

/**
 * @readonly
 * @type {number}
 */
export const INVALID_NEIGHBOUR = 0xFFFFFFFF;

/**
 * @readonly
 * @type {number}
 */
export const MAX_TET_INDEX = 0xFFFFFFFC;

/**
 * @readonly
 * @type {number}
 */
const DEFAULT_INITIAL_SIZE = 128;

/**
 * @readonly
 * @type {number}
 */
const CAPACITY_GROW_MULTIPLIER = 1.2;

/**
 * @readonly
 * @type {number}
 */
const CAPACITY_GROW_MIN_STEP = 32;

/**
 * Only keeps track of tetrahedra, actual point coordinates are stored outside.
 * For most useful operations point coordinates are passed in as an extra argument.
 *
 * Binary Layout:
 *      vertex_id_a :: uint32
 *      vertex_id_b :: uint32
 *      vertex_id_c :: uint32
 *      vertex_id_d :: uint32
 *      neighbour_a :: uint32         - neighbour tetrahedron, opposite to vertex A
 *      neighbour_b :: uint32         - neighbour tetrahedron, opposite to vertex B
 *      neighbour_c :: uint32         - neighbour tetrahedron, opposite to vertex C
 *      neighbour_d :: uint32         - neighbour tetrahedron, opposite to vertex D
 * Layout is similar to [1], but is interleaved for better cache locality.
 * Also note that sub-determinants are not included, these are only needed for building the mesh, we excluded them to keep structure clean and more compact.
 *
 * Neighbours are encoded in the following manner:
 *      MSB -> [tet_id:30bit][opposite_corner_index:2bit] <- LSB
 *      Code to get tet index: encoded >> 2
 *      Code to get corner index: encoded & 3
 *
 * @see [1] 2018 "One machine, one minute, three billion tetrahedra" by Célestin Marot,  Jeanne Pellerin and Jean-François Remacle
 * @see https://git.immc.ucl.ac.be/hextreme/hxt_seqdel (C source code for [1])
 */
export class TetrahedralMesh {
    /**
     *
     * @param {number} [initial_size]
     */
    constructor(initial_size = DEFAULT_INITIAL_SIZE) {
        assert.isNonNegativeInteger(initial_size, 'initial_size');

        /**
         *
         * @type {ArrayBuffer}
         * @private
         */
        this.__buffer = new ArrayBuffer(initial_size * LAYOUT_TETRA_BYTE_SIZE);

        /**
         *
         * @type {Uint32Array}
         * @private
         */
        this.__data_uint32 = new Uint32Array(this.__buffer);

        /**
         *
         * @type {DataView}
         * @private
         */
        this.__view = new DataView(this.__buffer);

        /**
         *
         * @type {number}
         * @private
         */
        this.__capacity = initial_size;

        /**
         *
         * @type {number}
         * @private
         */
        this.__used_end = 0;

        /**
         * Unused slots
         * @type {number[]}
         * @private
         */
        this.__free = [];

        /**
         *
         * @type {number}
         * @private
         */
        this.__free_pointer = 0;
    }


    /**
     * Access raw data
     * Useful for serialization
     * If you intend to modify the data directly - make sure you fully understand the implications of doing so
     * @returns {ArrayBuffer}
     */
    get data_buffer() {
        return this.__buffer;
    }

    /**
     * Exposes internal state, when this is false there are hole in the allocated memory
     * Useful mainly for serialization and debugging.
     * When serializing, you would want to get rid of any holes first by calling {@link compact}
     * @return {boolean}
     */
    get isCompacted() {
        return this.__free_pointer === 0;
    }

    /**
     * Traverse live tetrahedrons
     * @param { function( tet_id:number, mesh:TetrahedralMesh ):* } visitor
     * @param {*} [thisArg]
     */
    forEach(visitor, thisArg) {
        assert.isFunction(visitor, 'visitor');

        for (let i = 0; i < this.__used_end; i++) {
            if (!this.exists(i)) {
                continue;
            }

            visitor.call(thisArg, i, this);
        }
    }

    /**
     * Produces a list of live tetrahedrons
     * Allocates.
     * @return {number[]}
     */
    getLive() {
        /**
         *
         * @type {number[]}
         */
        const r = [];

        this.forEach((tet) => r.push(tet));

        return r;
    }

    /**
     * Clears all data from the mesh, making it contain 0 tetrahedrons
     * Ensures that consequent allocation requests will be sequential
     */
    clear() {

        // clear data
        this.__data_uint32.fill(0, 0, this.__used_end);

        // reset metadata
        this.__used_end = 0;
        this.__free_pointer = 0;
        this.__free.splice(0, this.__free.length);


    }

    /**
     *
     * @param {number} capacity
     */
    setCapacity(capacity) {
        assert.isNonNegativeInteger(capacity, 'capacity');

        if (capacity === this.__capacity) {
            // do nothing
            return;
        }

        if (capacity < this.__capacity && capacity < this.__used_end) {
            throw new Error('Reducing capacity would result in dropping information. This is an illegal operation. If you need to reduce capacity - either drop data or compact the layout first.');
        }

        // allocate new buffer
        const new_buffer = new ArrayBuffer(capacity * LAYOUT_TETRA_BYTE_SIZE);

        // move data across from old buffer to the new one
        const destination_uint8 = new Uint8Array(new_buffer);
        const source_uint8 = new Uint8Array(this.__buffer);

        typed_array_copy(source_uint8, destination_uint8);

        // set new buffer
        this.__buffer = new_buffer;
        this.__view = new DataView(new_buffer);
        this.__data_uint32 = new Uint32Array(new_buffer);

        // write new capacity
        this.__capacity = capacity;
    }

    /**
     *
     * @return {number}
     */
    getCapacity() {
        return this.__capacity;
    }

    /**
     * How many tetrahedrons are contained in the mesh, includes any unallocated tetrahedrons
     * @deprecated use {@link count} instead
     * @return {number}
     */
    size() {
        console.warn('Deprecated, use .count instead');

        return this.__used_end;
    }

    /**
     * Number of currently live tetrahedrons.
     * Excludes unallocated tetrahedrons.
     * @return {number}
     */
    get count() {
        return this.__used_end - this.__free_pointer;
    }


    /**
     * Grow capacity to at least the specified size
     * @private
     * @param {number} capacity minimum
     */
    growCapacity(capacity) {
        assert.isNonNegativeInteger(capacity, 'capacity');

        const existing_capacity = this.__capacity;

        const new_capacity = max3(
            capacity,
            Math.ceil(existing_capacity * CAPACITY_GROW_MULTIPLIER),
            existing_capacity + CAPACITY_GROW_MIN_STEP
        );

        this.setCapacity(new_capacity);
    }

    /**
     * Make sure that capacity is large enough to contain a certain total number of tetrahedrons
     * @param {number} capacity
     */
    ensureCapacity(capacity) {
        assert.isNonNegativeInteger(capacity, 'capacity');

        if (this.__capacity >= capacity) {
            // big enough
            return;
        }

        this.growCapacity(capacity);
    }

    /**
     * NOTE: this method can be quite slow in cases of sparse allocation, please prefer not to use it
     * @param {number} tet
     * @return {boolean}
     */
    exists(tet) {

        if (tet < 0 || tet >= this.__used_end) {
            return false;
        }

        for (let i = 0; i < this.__free_pointer; i++) {
            const free = this.__free[i];

            if (tet === free) {
                return false;
            }
        }

        return true;
    }

    /**
     * NOTE: the neighbour value must be encoded, see format specification for details
     * @param {number} tetra_index
     * @param {number} neighbour_index
     * @returns {number} index of the neighbour encoded with the opposite corner
     */
    getNeighbour(tetra_index, neighbour_index) {
        assert.isNonNegativeInteger(tetra_index, 'tetra_index');
        assert.ok(this.exists(tetra_index), 'tetrahedron does not exist');

        assert.isNonNegativeInteger(neighbour_index, 'neighbour_index');

        assert.lessThan(neighbour_index, 4, 'neighbour_index');

        const tetra_address = LAYOUT_TETRA_BYTE_SIZE * tetra_index;
        return this.__view.getUint32(tetra_address + (4 + neighbour_index) * 4);
    }

    /**
     * NOTE: the neighbour value must be encoded, see format specification for details
     * @param {number} tetra_index
     * @param {number} neighbour_index which neighbour to set (00..11)
     * @param {number} neighbour index of the neighbour encoded with the opposite corner
     */
    setNeighbour(tetra_index, neighbour_index, neighbour) {

        assert.isNonNegativeInteger(tetra_index, 'tetra_index');
        assert.ok(this.exists(tetra_index), 'tetrahedron does not exist');

        assert.isNonNegativeInteger(neighbour_index, 'neighbour_index');
        assert.isNonNegativeInteger(neighbour, 'neighbour');

        assert.lessThan(neighbour_index, 4, 'neighbour_index');

        const tetra_address = LAYOUT_TETRA_BYTE_SIZE * tetra_index;
        return this.__view.setUint32(tetra_address + (4 + neighbour_index) * 4, neighbour);
    }


    /**
     *
     * @param {number} tet_index
     * @param {number} point_index should be an integer between 0 and 3
     * @returns {number}
     */
    getVertexIndex(tet_index, point_index) {
        assert.isNonNegativeInteger(tet_index, 'tet_index');
        assert.lessThanOrEqual(tet_index, MAX_TET_INDEX, 'max index exceeded');
        //assert.ok(this.exists(tet_index), 'tetrahedron does not exist');

        assert.isNonNegativeInteger(point_index, 'point_index');
        assert.lessThan(point_index, 4, 'point_index must be less than 4');


        return this.__view.getUint32(tet_index * LAYOUT_TETRA_BYTE_SIZE + point_index * 4);
    }

    /**
     *
     * @param {number} tet_index
     * @param {number} point_index
     * @param {number} vertex
     */
    setVertexIndex(tet_index, point_index, vertex) {
        assert.isNonNegativeInteger(tet_index, 'tet_index');
        assert.lessThanOrEqual(tet_index, MAX_TET_INDEX, 'max index exceeded');
        //assert.ok(this.exists(tet_index), 'tetrahedron does not exist');

        assert.isNonNegativeInteger(point_index, 'point_index');
        assert.lessThan(point_index, 4, 'point_index must be less than 4');

        assert.isNonNegativeInteger(vertex, 'vertex');


        return this.__view.setUint32(
            tet_index * LAYOUT_TETRA_BYTE_SIZE + point_index * 4,
            vertex
        );
    }

    /**
     * Whether a given tetrahedron contains vertex with a given index
     * @param {number} tet
     * @param {number} vertex
     * @return {boolean}
     */
    tetContainsVertex(tet, vertex) {
        for (let i = 0; i < 4; i++) {
            if (this.getVertexIndex(tet, i) === vertex) {
                return true;
            }
        }

        return false;
    }


    /**
     * Allocate empty tet
     * NOTE: the tet memory might be dirty, please make sure you set/clear it as necessary
     * @return {number} index of allocated tetrahedron
     */
    allocate() {
        if (this.__free_pointer > 0) {
            this.__free_pointer--;
            return this.__free[this.__free_pointer];
        }

        const tetra_index = this.__used_end;

        this.__used_end++;

        if (tetra_index >= this.__capacity) {
            // needs to be increased in size
            this.growCapacity(tetra_index);
        }

        // initialize neighbours
        for (let i = 0; i < 4; i++) {
            this.setNeighbour(tetra_index, i, INVALID_NEIGHBOUR);
        }

        //assert(this.validateLength());


        // assert(this.validateLength());

        return tetra_index;
    }

    /**
     *
     * @param {number} a
     * @param {number} b
     * @param {number} c
     * @param {number} d
     * @returns {number} index of the new tetrahedron
     */
    append(a, b, c, d) {
        const tetra_index = this.allocate();

        const address = tetra_index * LAYOUT_TETRA_BYTE_SIZE;

        const view = this.__view;

        view.setUint32(address, a);
        view.setUint32(address + 4, b);
        view.setUint32(address + 8, c);
        view.setUint32(address + 12, d);

        // set neighbours
        view.setUint32(address + 16, INVALID_NEIGHBOUR);
        view.setUint32(address + 20, INVALID_NEIGHBOUR);
        view.setUint32(address + 24, INVALID_NEIGHBOUR);
        view.setUint32(address + 28, INVALID_NEIGHBOUR);

        return tetra_index;
    }

    /**
     * Sets back-links on neighbours to this tet to INVALID_NEIGHBOUR basically making them into mesh surface
     * This is a useful method for when you want to completely remove a given tet from the mesh to make sure that no dangling references will remain
     * @param {number} tetra_index
     */
    disconnect(tetra_index) {
        // find neighbours and remove reference to self
        for (let i = 0; i < 4; i++) {
            const neighbour_encoded = this.getNeighbour(tetra_index, i);

            if (neighbour_encoded === INVALID_NEIGHBOUR) {
                // no neighbour
                continue;
            }

            // get tetrahedra index and point index
            const neighbour_index = neighbour_encoded >> 2;
            const neighbour_point = neighbour_encoded & 3;

            // clear reference to self
            this.setNeighbour(neighbour_index, neighbour_point, INVALID_NEIGHBOUR);
        }
    }

    /**
     * Remove tetrahedron, de-allocating memory
     * Please note that if there are any dangling references in the mesh neighbourhood - you will need to take care of that separately
     * @param {number} tetra_index
     */
    delete(tetra_index) {
        assert.isNonNegativeInteger(tetra_index, 'tera_index');
        assert.lessThan(tetra_index, this.__used_end, 'attempting to remove tet outside of valid region');
        // assert.equal(this.__occupancy.get(tetra_index), true, 'tetrahedron does not exist');


        if (tetra_index === this.__used_end - 1) {
            // tet was at the end of the allocated space
            this.__used_end--;
        } else {
            // mark as dead
            this.__free[this.__free_pointer++] = tetra_index;
        }

        //assert(this.validateLength());

        //assert(this.validateLength());
    }

    /**
     * Used mainly to remove tetrahedrons whos points touch the "super-tetrahedron's" points that was inserted originally
     * These points are identified by an offset + count parameters
     * @param {number} range_start
     * @param {number} range_end
     */
    removeTetrasConnectedToPoints(range_start, range_end) {

        for (let i = this.__used_end - 1; i >= 0; i--) {

            for (let j = 0; j < 4; j++) {
                const point_index = this.getVertexIndex(i, j);

                if (point_index >= range_start && point_index <= range_end) {

                    if (!this.exists(i)) {
                        // tet doesn't actually exist (deallocated)
                        break;
                    }

                    // point index is in range, tetra should be removed
                    this.disconnect(i);
                    this.delete(i);
                    break;
                }
            }
        }

    }

    /**
     * Note that this method does not guarantee to find the containing tet in case of concave mesh, that is - if there is a gap between the starting tet and the countaining tet
     * @param {number} x
     * @param {number} y
     * @param {number} z
     * @param {number[]} points Positions of vertices of tetrahedrons
     * @param {number} [start_tetrahedron]
     * @returns {number} index of tetra or -1 if no containing tetra found
     */
    walkToTetraContainingPoint(x, y, z, points, start_tetrahedron = 0) {

        let entering_face = 4;

        let cur_tet = start_tetrahedron;

        let i;

        for (let steps_remaining = this.count + 1; steps_remaining > 0; steps_remaining--) {

            for (i = 0; i < 4; i++) {

                // we walk whenever the volume is positive
                const a_i = (i + 1) & 3;
                const b_i = (i & 2) ^ 3;
                const c_i = (i + 3) & 2;

                const a_index = this.getVertexIndex(cur_tet, a_i);
                const b_index = this.getVertexIndex(cur_tet, b_i);
                const c_index = this.getVertexIndex(cur_tet, c_i);

                const a3 = a_index * 3;
                const b3 = b_index * 3;
                const c3 = c_index * 3;

                const ax = points[a3];
                const ay = points[a3 + 1];
                const az = points[a3 + 2];

                const bx = points[b3];
                const by = points[b3 + 1];
                const bz = points[b3 + 2];

                const cx = points[c3];
                const cy = points[c3 + 1];
                const cz = points[c3 + 2];

                if (i !== entering_face && orient3d(ax, ay, az, bx, by, bz, cx, cy, cz, x, y, z) < 0.0) {

                    // point is outside the tet on the neighbour's side, move in that direction
                    const neighbour = this.getNeighbour(cur_tet, i);

                    if (neighbour === INVALID_NEIGHBOUR) {
                        // walked outside the mesh, point is not contained within
                        return -1;
                    }

                    // assert.notEqual(neighbour, INVALID_NEIGHBOUR, 'walked outside of the mesh');

                    cur_tet = neighbour >>> 2;
                    entering_face = neighbour & 3;

                    break;

                }
            }

            if (i === 4) {
                // point is inside the tet
                return cur_tet;
            }
        }

        throw new Error(`Failed to find tet, likely mesh is corrupted or non-convex`);
    }


    /**
     * Relocate tetrahedron in memory, patches neighbourhood links as well
     * NOTE: The destination slot will be overwritten. This is a dangerous method that can break the topology, make sure you fully understand what you are doing when using it
     * @param {number} source_index index of source tetrahedron
     * @param {number} destination_index new index, where the source tetrahedron is to be moved
     */
    relocate(source_index, destination_index) {
        assert.isNonNegativeInteger(source_index, 'source_index');
        assert.isNonNegativeInteger(destination_index, 'destination_index');

        if (source_index === destination_index) {
            // avoid unnecessary work
            return;
        }

        // validate_tetrahedron_neighbourhood(this, source_index, console.error);

        // patch neighbours
        for (let i = 0; i < 4; i++) {
            const encoded_neighbour = this.getNeighbour(source_index, i);

            if (encoded_neighbour === INVALID_NEIGHBOUR) {
                // no neighbour
                continue;
            }

            const neighbour_index = encoded_neighbour >> 2;
            const neighbour_vertex = encoded_neighbour & 3;

            const encoded_tet = (destination_index << 2) | (i & 3);

            assert.equal(this.getNeighbour(neighbour_index, neighbour_vertex), (source_index << 2) | (i & 3), 'invalid source state');

            this.setNeighbour(neighbour_index, neighbour_vertex, encoded_tet);
        }


        const layout_word_size = LAYOUT_TETRA_BYTE_SIZE >> 2;

        array_copy(
            this.__data_uint32, source_index * layout_word_size,
            this.__data_uint32, destination_index * layout_word_size,
            layout_word_size
        );

        // validate_tetrahedron_neighbourhood(this, destination_index, console.error);
    }

    /**
     * Perform compaction, removing unused memory slots
     * NOTE: existing tetrahedron indices can become invalidated as tets are moved into free slots
     * @returns {number} number of relocated elements
     */
    compact() {
        // sort free
        array_quick_sort_by_comparator(this.__free, number_compare_descending, null, 0, this.__free_pointer - 1);

        let relocation_count = 0;
        let free_head_pointer = 0;

        while (this.__free_pointer > free_head_pointer) {
            const last_used = this.__used_end - 1;

            if (this.__free[free_head_pointer] >= last_used) {
                /*
                 The slot is actually free.
                 As we have sorted the free slots, it's expected to be at the start of the list
                 Adjust pointers and continue onto the next slot
                 */
                free_head_pointer++
                this.__used_end = last_used;
                continue;
            }

            const free_slot = this.__free[this.__free_pointer - 1];

            this.__free_pointer--;

            if (last_used <= free_slot) {
                continue;
            }


            this.relocate(last_used, free_slot);
            relocation_count++;

            this.__used_end--;

        }

        this.__free.splice(0, this.__free.length); // release memory
        this.__free_pointer = 0;

        return relocation_count;
    }

    /**
     *
     * @param {BinaryBuffer} buffer
     */
    serialize(buffer) {
        buffer.writeUint32(1); // format version
        buffer.writeUintVar(this.__used_end);
        buffer.writeUintVar(this.__free_pointer);

        // record main buffer
        buffer.writeUint32Array(this.__data_uint32, 0, this.__used_end * LAYOUT_TETRA_WORD_COUNT);
        buffer.writeUint32Array(this.__free, 0, this.__free_pointer);
    }

    /**
     *
     * @param {BinaryBuffer} buffer
     */
    deserialize(buffer) {
        const version_number = buffer.readUint32();

        if (version_number !== 1) {
            throw new Error(`Unsupported version number, expected ${1}, instead got ${version_number}`);
        }

        this.__used_end = buffer.readUintVar();
        this.__free_pointer = buffer.readUintVar();

        this.ensureCapacity(this.__used_end);

        buffer.readUint32Array(this.__data_uint32, 0, this.__used_end * LAYOUT_TETRA_WORD_COUNT);

        buffer.readUint32Array(this.__free, 0, this.__free_pointer);
    }

    /**
     * Turns data into a base64 encoded string
     * @return {string}
     */
    serialize_base64() {
        const buffer = new BinaryBuffer();

        buffer.endianness = EndianType.LittleEndian;

        this.serialize(buffer);

        buffer.trim();

        return Base64.encode(buffer.data);
    }

    /**
     * Dual of serialization method, decodes a base64 representation
     * @param {string} str
     */
    deserialize_base64(str) {
        const array_buffer = Base64.decode(str);

        const buffer = BinaryBuffer.fromArrayBuffer(array_buffer);

        buffer.endianness = EndianType.LittleEndian;

        this.deserialize(buffer);
    }
}

/**
 * @readonly
 * @type {boolean}
 */
TetrahedralMesh.prototype.isTetrahedralMesh = true;
