import { assert } from "../../../core/assert.js";
import { BinaryDataType } from "../../../core/binary/type/BinaryDataType.js";
import { DataTypeByteSizes } from "../../../core/binary/type/DataTypeByteSizes.js";
import { objectKeyByValue } from "../../../core/model/object/objectKeyByValue.js";
import { computeStringHash } from "../../../core/primitives/strings/computeStringHash.js";

/**
 * Describes data structure of a single data attribute, such as a scalar, or a vector
 * This structure is designed to map well to GPU shader formats and is intended for larger datasets, such as meshes or particle clouds
 */
export class AttributeSpec {

    /**
     * Typically unique within a given set, used to identify the attribute
     * @type {string}
     */
    name = ""; // TODO move out into attribute

    /**
     * How the attribute is stored in memory and interpreted
     * @type {BinaryDataType}
     */
    type = BinaryDataType.Float32;

    /**
     * How many elements the attribute uses. This is cardinality of a vector.
     * 1 means the attribute is a scalar
     * must be greater or equal to 1
     * @type {number}
     */
    itemSize = 1;

    /**
     * Normalized values will be automatically converted to float32 in the shader
     * This gives good optimization opportunities for things like normal vectors
     * Applies to integer types only
     * @type {boolean}
     */
    normalized = false;

    /**
     * Utility constructor
     * @param {BinaryDataType} type
     * @param {number} itemSize
     * @param {boolean} [normalized]
     * @param {string} [name]
     * @returns {AttributeSpec}
     */
    static from(type, itemSize, normalized = false, name = "") {
        const spec = new AttributeSpec();

        spec.fromJSON({
            name,
            type,
            itemSize,
            normalized
        });

        return spec;
    }

    static fromJSON(j) {
        const r = new AttributeSpec();

        r.fromJSON(j);

        return r;
    }

    fromJSON({
                 name = "",
                 type,
                 itemSize,
                 normalized = false
             }) {

        assert.isString(name, 'name');
        assert.enum(type, BinaryDataType, 'type');

        assert.isNumber(itemSize, 'itemSize');
        assert.isNonNegativeInteger(itemSize, 'itemSize');
        assert.greaterThanOrEqual(itemSize, 1, 'itemSize must be >= 1');

        assert.isBoolean(normalized, 'normalized');

        this.name = name;
        this.type = type;
        this.itemSize = itemSize;
        this.normalized = normalized;
    }

    toJSON() {
        return {
            name: this.name,
            type: this.type,
            itemSize: this.itemSize,
            normalized: this.normalized
        };
    }

    toString() {
        let r = '';

        if (this.name !== "") {
            r += `${this.name}:`
        }

        r += `${this.itemSize}x${( objectKeyByValue(BinaryDataType, this.type) ?? this.type)}`;

        if (this.normalized) {
            r += '-norm';
        }

        return r;
    }

    hash() {
        return computeStringHash(this.name);
    }

    /**
     *
     * @param {AttributeSpec} other
     * @returns {boolean}
     */
    equals(other) {
        return this.type === other.type
            && this.itemSize === other.itemSize
            && this.normalized === other.normalized
            && this.name === other.name
            ;
    }

    /**
     *
     * @param {AttributeSpec} other
     */
    copy(other) {
        this.type = other.type;
        this.name = other.name;
        this.itemSize = other.itemSize;
        this.normalized = other.normalized;
    }

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

        r.copy(this);

        return r;
    }

    /**
     * How much space a single attribute value takes up
     * @returns {number} in bytes
     */
    getByteSize() {
        return this.itemSize * DataTypeByteSizes[this.type];
    }

    /**
     * Sorting comparator
     * @param {AttributeSpec} a
     * @param {AttributeSpec} b
     * @return {number}
     */
    static byName(a, b) {
        return a.name.localeCompare(b.name);
    }
}

/**
 * @readonly
 * @type {boolean}
 */
AttributeSpec.prototype.isAttributeSpec = true;
