import { assert } from "../../core/assert.js";

/**
 * Uniquely identifies an entity by both its ID and generation.
 * Lets us uniquely distinguish between two entities, even those with the same ID that were created at different times
 * @example
 *  // 1. Get your EntityComponentDataset to where you manager entities.
 *  const ecd = ... ; // EntityComponentDataset
 *
 *  // 2. Create an entity (we'll get an ID).
 *  const entityId = ecd.createEntity();
 *
 *  // 3. Create an EntityReference and bind it to the entity.
 *  const entityRef = EntityReference.bind(ecd, entityId);
 *
 *  // 4. After you no longer need the entity - destroy the entity via reference.
 *  entityRef.destroy(ecd);
 *
 * @see {@link EntityComponentDataset}
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class EntityReference {

    /**
     * Entity ID
     * When entity is live - this is the entity ID inside associated `EntityComponentDataset`, when the entity is not live - it's set to -1
     * @type {number}
     */
    id = -1

    /**
     * Entity generation number. This uniquely identifies an entity in combination with the ID
     * Generation of an existing entity must match for it to be considered "the same".
     * @see {@link EntityComponentDataset.getEntityGeneration}
     * @type {number}
     */
    generation = -1

    /**
     *
     * @param {EntityReference} other
     */
    copy(other) {
        assert.defined(other, 'other');

        this.id = other.id;
        this.generation = other.generation;
    }

    /**
     *
     * @returns {EntityReference}
     */
    clone() {
        const r = new EntityReference();

        r.copy(this);

        return r;
    }

    /**
     *
     * @return {number}
     */
    hash() {
        return (this.id * 31) ^ this.generation;
    }

    /**
     *
     * @param {EntityReference} other
     * @return {boolean}
     */
    equals(other) {
        return this.id === other.id
            && this.generation === other.generation
            ;
    }

    /**
     * Checks whether referenced entity exists and the generation matches
     * @param {EntityComponentDataset} ecd
     * @returns {boolean}
     */
    verify(ecd) {
        if (this.id < 0) {
            // special case
            return false;
        }

        return ecd.entityExists(this.id)
            && this.generation === ecd.getEntityGeneration(this.id)
            ;

    }

    /**
     * Destroys entity bound to this reference
     * If the reference is invalid for the given dataset - does nothing
     * @param {EntityComponentDataset} ecd
     * @returns {boolean} true if entity was destroyed, false otherwise
     */
    destroy(ecd) {
        if (this.verify(ecd)) {
            ecd.removeEntity(this.id);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Bind reference to a specific entity
     * @param {EntityComponentDataset} ecd
     * @param {number} entity
     */
    bind(ecd, entity) {
        assert.defined(ecd, 'ecd');
        assert.equal(ecd.isEntityComponentDataset, true, 'ecd.isEntityComponentDataset !== true');

        const generation = ecd.getEntityGeneration(entity);

        this.from(entity, generation);
    }

    /**
     *
     * @param {number} id
     * @param {number} generation
     */
    from(id, generation) {

        assert.isNonNegativeInteger(id, 'id');
        assert.isNonNegativeInteger(generation, 'generation');

        this.id = id;
        this.generation = generation;

    }

    /**
     *
     * @param {number} id
     * @param {number} generation
     * @return {EntityReference}
     */
    static from(id, generation) {
        const r = new EntityReference();

        r.from(id, generation);

        return r;
    }

    /**
     *
     * @param {EntityComponentDataset} ecd
     * @param {number} id
     * @returns {EntityReference}
     */
    static bind(ecd, id) {
        const r = new EntityReference();

        r.bind(ecd, id);

        return r;
    }
}

/**
 * @readonly
 * @type {boolean}
 */
EntityReference.prototype.isEntityReference = true;

/**
 * Special utility singleton to describe an invalid reference.
 * @readonly
 * @type {EntityReference}
 */
EntityReference.NULL = Object.freeze(new EntityReference());