import { FieldMask } from '@bufbuild/protobuf';
import { createClient } from '@connectrpc/connect';
import { createConnectTransport } from '@connectrpc/connect-web';
import { useThrelte } from '@threlte/core';
import {} from 'koota';
import { getContext, setContext } from 'svelte';
import { UuidTool } from 'uuid-tool';
import { writeBufferGeometryRange } from '../../attribute';
import { DrawService } from '../../buf/draw/v1/service_connect';
import { EntityChangeType, StreamEntityChangesResponse } from '../../buf/draw/v1/service_pb';
import { asFloat32Array, inMeters, STRIDE } from '../../buffer';
import { drawDrawing, drawTransform, updateDrawing, updateModel, updateTransform, } from '../../draw';
import { hierarchy, traits, useWorld } from '../../ecs';
import { useCameraControls } from '../../hooks/useControls.svelte';
import { createServerRelationships } from './serverRelationships';
import { useDrawConnectionConfig } from './useDrawConnectionConfig.svelte';
const DRAW_SERVICE_KEY = Symbol('draw-service-context');
const FLOAT32_SIZE = 4;
const ConnectionStatus = {
    CONNECTED: 'connected',
    DISCONNECTED: 'disconnected',
    CONNECTING: 'connecting',
};
export function provideDrawService() {
    const { invalidate } = useThrelte();
    const world = useWorld();
    const cameraControls = useCameraControls();
    const drawConnectionConfig = useDrawConnectionConfig();
    const serverRelationships = createServerRelationships();
    let connectionStatus = $state(ConnectionStatus.DISCONNECTED);
    const url = $derived(drawConnectionConfig.current?.backendIP
        ? `http://${drawConnectionConfig.current.backendIP}:3030`
        : undefined);
    const transformEntities = new Map();
    const drawingEntities = new Map();
    let pendingEvents = [];
    let flushScheduled = false;
    let activeClient;
    let activeSignal;
    const activeChunkPulls = new Set();
    const destroyTransform = (uuidStr) => {
        const entity = transformEntities.get(uuidStr);
        if (!entity)
            return;
        if (world.has(entity))
            entity.destroy();
        transformEntities.delete(uuidStr);
    };
    const destroyDrawing = (uuidStr) => {
        const entity = drawingEntities.get(uuidStr);
        if (!entity)
            return;
        hierarchy.destroyEntityTree(world, entity);
        drawingEntities.delete(uuidStr);
    };
    const processEvent = (event) => {
        const { changeType, entity, uuid } = event;
        if (entity.case === 'transform') {
            processTransformEvent(entity.value, changeType, uuid);
        }
        else if (entity.case === 'drawing') {
            processDrawingEvent(entity.value, changeType, uuid);
        }
        invalidate();
    };
    const processTransformEvent = (transform, changeType, uuid) => {
        if (changeType === EntityChangeType.ADDED) {
            if (!transformEntities.has(uuid)) {
                const spawned = drawTransform(world, transform, traits.DrawServiceAPI);
                serverRelationships.apply(spawned.entity, uuid, spawned.relationships);
                transformEntities.set(uuid, spawned.entity);
            }
        }
        else if (changeType === EntityChangeType.REMOVED) {
            serverRelationships.forget(uuid);
            destroyTransform(uuid);
        }
        else if (changeType === EntityChangeType.UPDATED) {
            const existing = transformEntities.get(uuid);
            if (existing) {
                const updated = updateTransform(existing, transform);
                serverRelationships.apply(updated.entity, uuid, updated.relationships);
            }
            else {
                const spawned = drawTransform(world, transform, traits.DrawServiceAPI);
                serverRelationships.apply(spawned.entity, uuid, spawned.relationships);
                transformEntities.set(uuid, spawned.entity);
            }
        }
    };
    const isChunkedDrawing = (drawing) => {
        return drawing.metadata?.chunks !== undefined && drawing.metadata.chunks.total > 0;
    };
    const getChunkInfo = (drawing) => {
        const meta = drawing.metadata?.chunks;
        if (!meta || meta.total === 0)
            return undefined;
        const shape = drawing.physicalObject?.geometryType;
        if (shape?.case === 'points') {
            const chunkElements = shape.value.positions.length / (STRIDE.POSITIONS * FLOAT32_SIZE);
            return {
                total: meta.total,
                firstEnd: chunkElements,
            };
        }
        return undefined;
    };
    const pullChunks = async (client, uuid, uuidBytes, entity, totalElements, firstChunkEnd, signal) => {
        if (activeChunkPulls.has(uuid))
            return;
        activeChunkPulls.add(uuid);
        try {
            let nextStart = firstChunkEnd;
            while (!signal.aborted) {
                const response = await client.getEntityChunk({ uuid: uuidBytes, start: nextStart }, { signal });
                // done with no payload is the server's "past end" sentinel (startByte >= posLen), not the final real chunk
                if (response.done && !response.entity.value)
                    break;
                const drawing = response.entity.case === 'drawing' ? response.entity.value : undefined;
                if (!drawing)
                    break;
                const shape = drawing.physicalObject?.geometryType;
                if (shape?.case !== 'points')
                    break;
                const buffer = entity.get(traits.BufferGeometry);
                if (!buffer)
                    break;
                const positions = asFloat32Array(shape.value.positions, inMeters);
                const metadata = drawing.metadata;
                if (!metadata)
                    break;
                writeBufferGeometryRange(buffer, positions, response.start, metadata);
                const chunkElements = positions.length / 3;
                nextStart = response.start + chunkElements;
                entity.set(traits.ChunkProgress, { loaded: nextStart, total: totalElements });
                invalidate();
                if (response.done)
                    break;
            }
        }
        catch (error) {
            if (!signal.aborted) {
                console.error(`Chunk pull failed for entity ${uuid}:`, error);
            }
        }
        finally {
            activeChunkPulls.delete(uuid);
            if (world.has(entity)) {
                entity.remove(traits.ChunkProgress);
            }
        }
    };
    const processDrawingEvent = (drawing, changeType, uuid) => {
        if (changeType === EntityChangeType.ADDED) {
            if (!drawingEntities.has(uuid)) {
                const spawned = drawDrawing(world, drawing, traits.DrawServiceAPI);
                serverRelationships.apply(spawned.entity, uuid, spawned.relationships);
                drawingEntities.set(uuid, spawned.entity);
                if (isChunkedDrawing(drawing) && activeClient && activeSignal) {
                    const chunk = getChunkInfo(drawing);
                    if (chunk) {
                        spawned.entity.add(traits.ChunkProgress({ loaded: chunk.firstEnd, total: chunk.total }));
                        const uuidBytes = drawing.uuid ?? new Uint8Array();
                        void pullChunks(activeClient, uuid, uuidBytes, spawned.entity, chunk.total, chunk.firstEnd, activeSignal);
                    }
                }
            }
        }
        else if (changeType === EntityChangeType.REMOVED) {
            serverRelationships.forget(uuid);
            destroyDrawing(uuid);
        }
        else if (changeType === EntityChangeType.UPDATED) {
            const existing = drawingEntities.get(uuid);
            if (existing) {
                const isModel = drawing.physicalObject?.geometryType?.case === 'model';
                const result = isModel
                    ? updateModel(world, existing, drawing, traits.DrawServiceAPI)
                    : updateDrawing(world, existing, drawing);
                serverRelationships.apply(result.entity, uuid, result.relationships);
                drawingEntities.set(uuid, result.entity);
            }
            else {
                const spawned = drawDrawing(world, drawing, traits.DrawServiceAPI);
                serverRelationships.apply(spawned.entity, uuid, spawned.relationships);
                drawingEntities.set(uuid, spawned.entity);
            }
        }
    };
    const applyEvents = (events) => {
        const eventsByUUID = new Map();
        for (const event of events) {
            const existing = eventsByUUID.get(event.uuid);
            if (!existing) {
                eventsByUUID.set(event.uuid, event);
                continue;
            }
            switch (event.changeType) {
                case EntityChangeType.REMOVED: {
                    eventsByUUID.set(event.uuid, event);
                    break;
                }
                case EntityChangeType.ADDED: {
                    if (existing.changeType !== EntityChangeType.REMOVED) {
                        eventsByUUID.set(event.uuid, event);
                    }
                    break;
                }
                case EntityChangeType.UPDATED: {
                    if (existing.changeType === EntityChangeType.ADDED) {
                        existing.entity = event.entity;
                    }
                    else if (existing.changeType === EntityChangeType.UPDATED) {
                        existing.updatedFields ??= new FieldMask();
                        const paths = event.updatedFields?.paths ?? [];
                        for (const path of paths) {
                            if (!existing.updatedFields.paths.includes(path)) {
                                existing.updatedFields.paths.push(path);
                            }
                        }
                        existing.entity = event.entity;
                    }
                    else {
                        eventsByUUID.set(event.uuid, event);
                    }
                    break;
                }
            }
        }
        for (const event of eventsByUUID.values()) {
            processEvent(event);
        }
    };
    const scheduleFlush = () => {
        if (flushScheduled)
            return;
        flushScheduled = true;
        requestAnimationFrame(() => {
            flushScheduled = false;
            const toApply = pendingEvents;
            pendingEvents = [];
            applyEvents(toApply);
        });
    };
    const streamEntityChanges = async (client, signal) => {
        try {
            for await (const response of client.streamEntityChanges({}, { signal })) {
                connectionStatus = ConnectionStatus.CONNECTED;
                const { entity } = response;
                if (!entity.case)
                    continue;
                const uuid = UuidTool.toString([...(entity.value.uuid ?? [])]);
                pendingEvents.push({
                    uuid,
                    changeType: response.changeType,
                    entity,
                    updatedFields: response.updatedFields,
                });
                scheduleFlush();
            }
        }
        catch (error) {
            if (!signal.aborted) {
                console.error('Draw service entity stream error:', error);
                connectionStatus = ConnectionStatus.DISCONNECTED;
            }
        }
    };
    const streamSceneChanges = async (client, signal) => {
        try {
            for await (const response of client.streamSceneChanges({}, { signal })) {
                const { sceneMetadata } = response;
                if (!sceneMetadata)
                    continue;
                if (sceneMetadata.sceneCamera?.position && sceneMetadata.sceneCamera?.lookAt) {
                    const { position, lookAt, animated } = sceneMetadata.sceneCamera;
                    cameraControls.setPose({
                        position: [position.x * 0.001, position.y * 0.001, position.z * 0.001],
                        lookAt: [lookAt.x * 0.001, lookAt.y * 0.001, lookAt.z * 0.001],
                    }, animated ?? false);
                }
            }
        }
        catch (error) {
            if (!signal.aborted) {
                console.error('Draw service scene stream error:', error);
            }
        }
    };
    $effect(() => {
        if (!url) {
            connectionStatus = ConnectionStatus.DISCONNECTED;
            activeClient = undefined;
            return;
        }
        const controller = new AbortController();
        connectionStatus = ConnectionStatus.CONNECTING;
        const transport = createConnectTransport({ baseUrl: url });
        const client = createClient(DrawService, transport);
        activeClient = client;
        activeSignal = controller.signal;
        void streamEntityChanges(client, controller.signal);
        void streamSceneChanges(client, controller.signal);
        return () => {
            controller.abort();
            activeClient = undefined;
            activeSignal = undefined;
            connectionStatus = ConnectionStatus.DISCONNECTED;
            activeClient = undefined;
            for (const entity of transformEntities.values()) {
                hierarchy.destroyEntityTree(world, entity);
            }
            transformEntities.clear();
            for (const entity of drawingEntities.values()) {
                hierarchy.destroyEntityTree(world, entity);
            }
            drawingEntities.clear();
            serverRelationships.reset();
        };
    });
    $effect(() => () => serverRelationships.dispose());
    setContext(DRAW_SERVICE_KEY, {
        get connectionStatus() {
            return connectionStatus;
        },
    });
}
export function useDrawService() {
    return getContext(DRAW_SERVICE_KEY);
}
