import { makeObservable, Disposable, releaseObject, retainObject } from '@zephyr3d/base';
import { PBPrimitiveType, matchVertexBuffer } from '@zephyr3d/device';
import { RenderBundleWrapper } from './renderbundle_wrapper.js';
import { getDevice } from '../app/api.js';

/**
 * Holds vertex/index data and draw parameters for a mesh geometry.
 *
 * Responsibilities:
 * - Owns one or more vertex buffers and an optional index buffer.
 * - Defines primitive topology, draw range (start/count), and vertex layout.
 * - Provides utilities to create/set/remove buffers and query vertex/face counts.
 * - Tracks changes via a change tag and notifies render-bundle caching.
 * - Optionally stores a bounding volume and supports ray intersection tests.
 *
 * Ownership and lifecycle:
 * - When adding buffers (`setVertexBuffer`, `setIndexBuffer`), the primitive retains them.
 * - When removing/overwriting buffers, the primitive releases the previous buffers.
 * - Disposing the primitive also disposes the internal `VertexLayout` and releases retained buffers.
 *
 * @public
 */ class Primitive extends makeObservable(Disposable)() {
    /** @internal Current vertex layout object (created lazily from options). */ _vertexLayout;
    /** @internal Mutable options used to build the vertex layout. */ _vertexLayoutOptions;
    /** @internal Primitive topology (e.g., 'triangle-list', 'line-strip'). */ _primitiveType;
    /** @internal First index/vertex to draw. */ _indexStart;
    /** @internal Number of indices/vertices to draw (computed lazily if null). */ _indexCount;
    /** @internal Cached default index count (derived when needed). */ _defaultIndexCount;
    /** @internal Marks layout dirty when buffers/topology change. */ _vertexLayoutDirty;
    /** @internal Monotonic runtime id. */ static _nextId = 0;
    /** @internal Unique runtime id for the instance. */ _id;
    /** @internal Optional bounding volume for culling/raycast. */ _bbox;
    /** @internal Change tag increments when draw-affecting state changes. */ _changeTag;
    /**
   * Create an empty primitive.
   *
   * Defaults:
   * - Primitive type: 'triangle-list'
   * - `indexStart = 0`, `indexCount = null` (auto-computed)
   * - No vertex/index buffers attached
   * - No bounding volume
   */ constructor(){
        super();
        this._vertexLayout = null;
        this._vertexLayoutOptions = {
            vertexBuffers: []
        };
        this._primitiveType = 'triangle-list';
        this._indexStart = 0;
        this._indexCount = null;
        this._defaultIndexCount = 0;
        this._vertexLayoutDirty = false;
        this._id = ++Primitive._nextId;
        this._changeTag = 0;
        this._bbox = null;
    }
    /**
   * Unique runtime identifier of this primitive.
   *
   * @returns The numeric instance id.
   * @public
   */ get id() {
        return this._id;
    }
    /**
   * Change tag that increments whenever draw-affecting state changes.
   * Useful for invalidating cached render bundles.
   *
   * @returns The current change tag value.
   */ get changeTag() {
        return this._changeTag;
    }
    /**
   * Create a shallow clone: copies topology, draw range, and buffers.
   *
   * Note: Buffers are re-retained on the new primitive.
   *
   * @returns A cloned Primitive instance.
   */ clone() {
        const other = new Primitive();
        other.copyFrom(this);
        return other;
    }
    /**
   * Copy from another primitive.
   *
   * Copies:
   * - All vertex buffers and the index buffer
   * - Primitive type, index start, index count
   *
   * @param other - The source primitive to copy from.
   * @returns void
   */ copyFrom(other) {
        for (const info of other._vertexLayoutOptions.vertexBuffers){
            this.setVertexBuffer(info.buffer, info.stepMode);
        }
        this.setIndexBuffer(other._vertexLayoutOptions.indexBuffer ?? null);
        this.primitiveType = other.primitiveType;
        this.indexStart = other.indexStart;
        this.indexCount = other.indexCount;
    }
    /**
   * Primitive topology.
   */ get primitiveType() {
        return this._primitiveType;
    }
    set primitiveType(type) {
        if (type !== this._primitiveType) {
            this._primitiveType = type;
            this._changeTag++;
            RenderBundleWrapper.primitiveChanged(this);
        }
    }
    /**
   * Starting index/vertex for drawing.
   */ get indexStart() {
        return this._indexStart;
    }
    set indexStart(val) {
        if (val !== this._indexStart) {
            this._indexStart = val;
            this._changeTag++;
            RenderBundleWrapper.primitiveChanged(this);
        }
    }
    /**
   * Number of indices/vertices to draw.
   */ get indexCount() {
        this._indexCount = this._indexCount ?? this.calcDefaultIndexCount();
        return this._indexCount;
    }
    set indexCount(val) {
        if (val !== this._indexCount) {
            this._indexCount = val;
            this._changeTag++;
            RenderBundleWrapper.primitiveChanged(this);
        }
    }
    /**
   * Query total vertex count from the position buffer, if present.
   *
   * @returns Total vertex count; 0 if no position buffer is set.
   */ getNumVertices() {
        const posInfo = this.getVertexBufferInfo('position');
        return posInfo?.buffer ? posInfo.buffer.byteLength / posInfo.stride >> 0 : 0;
    }
    /**
   * Query total face/segment count based on topology and buffer size.
   *
   * - For indexed geometry: derived from index buffer.
   * - For non-indexed: derived from position vertex count.
   *
   * @returns Total primitive count for the current topology.
   */ getNumFaces() {
        const ib = this.getIndexBuffer();
        const count = ib ? ib.byteLength >> (ib.indexType.primitiveType === PBPrimitiveType.U16 ? 1 : 2) : this.getNumVertices();
        switch(this.primitiveType){
            case 'line-list':
                return count >> 1;
            case 'point-list':
                return count;
            case 'line-strip':
                return count - 1;
            case 'triangle-fan':
                return count - 2;
            case 'triangle-strip':
                return count - 2;
            case 'triangle-list':
                return count / 3 >> 0;
            default:
                return 0;
        }
    }
    /**
   * Remove all vertex buffers that match a given semantic.
   *
   * This releases retained buffers, marks the layout dirty, and invalidates bundles.
   *
   * @param semantic - The vertex semantic to remove (e.g., 'position', 'normal').
   * @returns void
   */ removeVertexBuffer(semantic) {
        for(let i = this._vertexLayoutOptions.vertexBuffers.length - 1; i >= 0; i--){
            const info = this._vertexLayoutOptions.vertexBuffers[i];
            if (matchVertexBuffer(info.buffer, semantic)) {
                releaseObject(info.buffer);
                this._vertexLayoutOptions.vertexBuffers.splice(i, 1);
                this._vertexLayoutDirty = true;
            }
        }
        if (this._vertexLayoutDirty) {
            this._changeTag++;
            RenderBundleWrapper.primitiveChanged(this);
        }
    }
    /**
   * Get the vertex buffer that matches a given semantic.
   *
   * @param semantic - The vertex semantic to look up.
   * @returns The matching vertex buffer, or `null` if not found.
   */ getVertexBuffer(semantic) {
        for (const info of this._vertexLayoutOptions.vertexBuffers){
            if (info.buffer && matchVertexBuffer(info.buffer, semantic)) {
                return info.buffer;
            }
        }
        return null;
    }
    /**
   * Get vertex buffer information for a given semantic.
   *
   * @param semantic - The vertex semantic to look up.
   * @returns The `VertexBufferInfo`, or `null` if not found.
   */ getVertexBufferInfo(semantic) {
        this.checkVertexLayout();
        return this._vertexLayout?.getVertexBufferInfo(semantic) ?? null;
    }
    /**
   * Create a vertex buffer from data and add it to the primitive.
   *
   * - For interleaved layouts, pass an array of `VertexAttribFormat`.
   * - For a single attribute, pass a single `VertexAttribFormat`.
   *
   * @param format - Vertex attribute format(s).
   * @param data - Typed array with vertex data.
   * @param stepMode - Optional step mode (e.g., 'vertex', 'instance').
   * @returns The created `StructuredBuffer`.
   */ createAndSetVertexBuffer(format, data, stepMode) {
        const device = getDevice();
        const buffer = Array.isArray(format) ? device.createInterleavedVertexBuffer(format, data) : device.createVertexBuffer(format, data);
        return this.setVertexBuffer(buffer, stepMode);
    }
    /**
   * Add an existing vertex buffer to the primitive.
   *
   * Ownership note: The primitive retains the buffer; it will be released or disposed when replaced or on dispose.
   *
   * @param buffer - The vertex buffer to add.
   * @param stepMode - Optional step mode for the buffer.
   * @returns The same buffer.
   */ setVertexBuffer(buffer, stepMode) {
        retainObject(buffer);
        this._vertexLayoutOptions.vertexBuffers.push({
            buffer,
            stepMode
        });
        this._vertexLayoutDirty = true;
        this._changeTag++;
        RenderBundleWrapper.primitiveChanged(this);
        return buffer;
    }
    /**
   * Create an index buffer from data and set it on the primitive.
   *
   * @param data - Index data as Uint16Array or Uint32Array.
   * @param dynamic - Whether the index buffer is dynamic (unmanaged).
   * @returns The created `IndexBuffer`.
   */ createAndSetIndexBuffer(data, dynamic) {
        const device = getDevice();
        const buffer = device.createIndexBuffer(data, {
            dynamic: !!dynamic,
            managed: !dynamic
        });
        this.setIndexBuffer(buffer);
        return buffer;
    }
    /**
   * Set or replace the index buffer.
   *
   * Ownership note: The primitive retains the buffer; previous buffer is released.
   * Marks the vertex layout dirty and invalidates bundles.
   *
   * @param buffer - The index buffer to set (non-null).
   * @returns void
   */ setIndexBuffer(buffer) {
        if (this._vertexLayoutOptions.indexBuffer !== buffer) {
            retainObject(buffer);
            releaseObject(this._vertexLayoutOptions.indexBuffer ?? null);
            this._vertexLayoutOptions.indexBuffer = buffer;
            this._vertexLayoutDirty = true;
            this._changeTag++;
            RenderBundleWrapper.primitiveChanged(this);
        }
    }
    /**
   * Get the current index buffer.
   *
   * @returns The index buffer, or `undefined`/`null` if none set.
   */ getIndexBuffer() {
        return this._vertexLayoutOptions.indexBuffer ?? null;
    }
    /**
   * Issue a non-instanced draw for the current topology and range.
   *
   * Preconditions: A valid vertex layout and `indexCount > 0`.
   */ draw() {
        this.checkVertexLayout();
        if (this.indexCount > 0) {
            this._vertexLayout?.draw(this._primitiveType, this._indexStart, this.indexCount);
        }
    }
    /**
   * Issue an instanced draw for the current topology and range.
   *
   * Preconditions: A valid vertex layout and `indexCount > 0`.
   *
   * @param numInstances - Number of instances to draw.
   */ drawInstanced(numInstances) {
        this.checkVertexLayout();
        if (this.indexCount > 0) {
            this._vertexLayout?.drawInstanced(this._primitiveType, this._indexStart, this.indexCount, numInstances);
        }
    }
    /**
   * Dispose this primitive and release associated GPU resources.
   */ onDispose() {
        super.onDispose();
        this._vertexLayout?.dispose();
        this._vertexLayout = null;
        if (this._vertexLayoutOptions) {
            releaseObject(this._vertexLayoutOptions.indexBuffer ?? null);
            for (const info of this._vertexLayoutOptions.vertexBuffers){
                releaseObject(info.buffer);
            }
        }
    }
    /*
  createAABBTree(): AABBTree {
    const indices = this.getIndexBuffer() ? this.getIndexBuffer().getData() : null;
    const vertices = (this.getVertexBuffer(VERTEX_ATTRIB_POSITION)?.getData() as Float32Array) || null;
    const aabbtree = new AABBTree();
    aabbtree.buildFromPrimitives(vertices, indices, this._primitiveType);
    return aabbtree;
  }
  */ /**
   * Get the bounding volume associated with this primitive.
   *
   * @returns The current bounding volume, or `null` if not set.
   */ getBoundingVolume() {
        return this._bbox;
    }
    /**
   * Set or replace the bounding volume of this primitive.
   *
   * Triggers registered bounding-volume change callbacks.
   *
   * @param bv - The bounding volume to set.
   * @returns void
   */ setBoundingVolume(bv) {
        if (bv !== this._bbox) {
            this._bbox = bv;
            this.dispatchEvent('bv_changed');
        }
    }
    /**
   * Test intersection against the current axis-aligned bounding box (AABB).
   *
   * @param ray - Ray to test against the primitive's AABB (derived from its bounding volume).
   * @returns The distance from ray origin to the intersection, or `null` if no hit or no AABB.
   */ raycast(ray) {
        const aabb = this.getBoundingVolume()?.toAABB();
        return aabb ? ray.bboxIntersectionTestEx(aabb) : null;
    }
    /** @internal */ checkVertexLayout() {
        if (this._vertexLayoutDirty) {
            this._vertexLayout?.dispose();
            const device = getDevice();
            this._vertexLayout = device.createVertexLayout(this._vertexLayoutOptions);
            this._vertexLayoutDirty = false;
        }
    }
    /** @internal */ calcDefaultIndexCount() {
        const indexBuffer = this.getIndexBuffer();
        if (indexBuffer) {
            return Math.max(0, indexBuffer.length - this._indexStart);
        }
        const info = this.getVertexBufferInfo('position');
        if (info) {
            return Math.max(0, Math.floor((info.buffer.byteLength - info.drawOffset) / info.stride) - this._indexStart);
        }
        return 0;
    }
}

export { Primitive };
//# sourceMappingURL=primitive.js.map
