import '../../../scene/scene.js';
import '@zephyr3d/base';
import { getDevice } from '../../../app/api.js';
import '../../../values.js';
import '../../../render/render_queue.js';
import '../../../render/primitive.js';
import '@zephyr3d/device';
import '../../../material/shader/helper.js';
import '../../../material/lambert.js';
import '../../../material/blinn.js';
import '../../../material/unlit.js';
import '../../../material/particle.js';
import '../../../material/meshmaterial.js';
import '../../../material/grassmaterial.js';
import '../../../material/terrain-cm.js';
import '../../../material/pbrmr.js';
import '../../../material/pbrsg.js';
import '../../../material/pbrblueprint.js';
import '../../../material/sprite.js';
import '../../../material/sprite_std.js';
import { defineProps } from '../types.js';
import '../../blueprint/material/texture.js';
import '../../../render/rendergraph/forward_plus_builder.js';
import '../../../render/sky.js';
import '../../../render/clipmap.js';
import '../../../shaders/atmosphere.js';
import { GraphNode } from '../../../scene/graph_node.js';
import '../../../shadow/shadowmapper.js';
import '../../../scene/mesh.js';
import '../../../scene/basesprite.js';
import '../../../scene/water.js';
import '../../../scene/scene_node.js';
import '../../../scene/particlesys.js';
import '../../../scene/batchgroup.js';
import { ClipmapTerrain } from '../../../scene/terrain-cm/terrain-cm.js';
import '../../../scene/terrain-cm/grass.js';
import '../../../scene/raycast_visitor.js';
import { JSONArray } from '../json.js';

function mergeTypedArrays(ctor, arrays) {
    const totalLength = arrays.reduce((acc, arr)=>acc + arr.length, 0);
    const result = new ctor(totalLength);
    let offset = 0;
    for (const array of arrays){
        result.set(array, offset);
        offset += array.length;
    }
    return result;
}
async function getTerrainGrassContent(terrain) {
    const grassRenderer = terrain.grassRenderer;
    const layerDatas = [];
    let dataSize = 4 + 4 * grassRenderer.numLayers;
    for(let i = 0; i < grassRenderer.numLayers; i++){
        const promises = [];
        const layer = grassRenderer.getLayer(i);
        const queue = [
            layer.quadtree
        ];
        while(queue.length > 0){
            const quadtreeNode = queue.shift();
            if (quadtreeNode.children) {
                queue.push(...quadtreeNode.children);
            }
            const grassInstances = quadtreeNode.grassInstances;
            if (grassInstances.numInstances > 0) {
                const instanceBuffer = grassInstances.instanceBuffer;
                const P = instanceBuffer.getBufferSubData(null, 0, grassInstances.numInstances * 4 * 4);
                promises.push(P);
            }
        }
        if (promises.length > 0) {
            const data = await Promise.all(promises);
            const merged = mergeTypedArrays(Uint8Array, data);
            dataSize += merged.length;
            layerDatas.push(merged);
        } else {
            layerDatas.push(new Uint8Array());
        }
    }
    const data = new DataView(new ArrayBuffer(dataSize));
    const view = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
    let offset = 0;
    data.setUint32(offset, grassRenderer.numLayers, true);
    offset += 4;
    for(let i = 0; i < grassRenderer.numLayers; i++){
        data.setUint32(offset, layerDatas[i].length, true);
        offset += 4;
        view.set(layerDatas[i], offset);
        offset += layerDatas[i].length;
    }
    return data.buffer;
}
async function getTerrainHeightMapContent(terrain) {
    const heightmap = terrain.heightMap;
    const buffer = new ArrayBuffer(2 * 4 + heightmap.width * heightmap.height * 2);
    const head = new DataView(buffer);
    head.setUint32(0, heightmap.width, true);
    head.setUint32(4, heightmap.height, true);
    const data = new Uint16Array(buffer, 2 * 4);
    if (heightmap.format === 'r16f') {
        await heightmap.readPixels(0, 0, heightmap.width, heightmap.height, 0, 0, data);
    } else if (heightmap.format === 'rgba16f') {
        // WebGL1 uses rgba16f for height map, so we need to convert
        const tmpData = new Uint16Array(heightmap.width * heightmap.height * 4);
        await heightmap.readPixels(0, 0, heightmap.width, heightmap.height, 0, 0, tmpData);
        for(let i = 0; i < heightmap.width * heightmap.height; i++){
            data[i] = tmpData[i * 4 + 0]; // Use the red channel as height
        }
    }
    return buffer;
}
async function getTerrainSplatMapContent(terrain) {
    const device = getDevice();
    const splatMap = terrain.splatMap;
    const numLayers = terrain.material.numDetailMaps + 3 >> 2;
    const info = device.getDeviceCaps().textureCaps.getTextureFormatInfo(splatMap.format);
    const buffer = new ArrayBuffer(3 * 4 + numLayers * splatMap.width * splatMap.height * info.blockWidth * info.blockHeight * info.size);
    const head = new DataView(buffer);
    head.setUint32(0, splatMap.width, true);
    head.setUint32(4, splatMap.height, true);
    head.setUint32(8, numLayers, true);
    for(let i = 0; i < numLayers; i++){
        const layerData = new Uint8Array(buffer, 3 * 4 + i * splatMap.width * splatMap.height * info.blockWidth * info.blockHeight * info.size);
        await splatMap.readPixels(0, 0, splatMap.width, splatMap.height, 0, 0, layerData);
    }
    return buffer;
}
/** @internal */ function getTerrainClass(manager) {
    return {
        ctor: ClipmapTerrain,
        name: 'ClipmapTerrain',
        parent: GraphNode,
        createFunc (ctx, init) {
            const node = new ClipmapTerrain(ctx.scene);
            node.numDetailMaps = init;
            node.parent = ctx;
            return {
                obj: node
            };
        },
        getInitParams (obj) {
            return obj.numDetailMaps;
        },
        getProps () {
            return defineProps([
                {
                    name: 'Resolution',
                    description: 'Terrain resolution',
                    type: 'int2',
                    default: [
                        256,
                        256
                    ],
                    options: {
                        minValue: 1,
                        maxValue: 4096
                    },
                    get (value) {
                        value.num[0] = this.sizeX;
                        value.num[1] = this.sizeZ;
                    },
                    set (value) {
                        this.setSize(value.num[0], value.num[1]);
                    }
                },
                {
                    name: 'CastShadow',
                    description: 'If true, the terrain can cast shadows',
                    type: 'bool',
                    default: true,
                    get (value) {
                        value.bool[0] = this.castShadow;
                    },
                    set (value) {
                        this.castShadow = value.bool[0];
                    }
                },
                {
                    name: 'Wireframe',
                    description: 'If true, the terrain will be rendered as wireframe',
                    type: 'bool',
                    default: false,
                    isPersistent () {
                        return false;
                    },
                    get (value) {
                        value.bool[0] = this.wireframe;
                    },
                    set (value) {
                        this.wireframe = value.bool[0];
                    }
                },
                {
                    name: 'Debug',
                    description: 'Debug visualization mode for the terrain material',
                    type: 'string',
                    options: {
                        enum: {
                            labels: [
                                'None',
                                'UV',
                                'VertexNormal',
                                'DetailNormal',
                                'Tangent',
                                'Binormal',
                                'Albedo'
                            ],
                            values: [
                                'none',
                                'uv',
                                'vertex_normal',
                                'detail_normal',
                                'tangent',
                                'bitangent',
                                'albedo'
                            ]
                        }
                    },
                    default: 'none',
                    isPersistent () {
                        return false;
                    },
                    get (value) {
                        value.str[0] = this.material.debugMode;
                    },
                    set (value) {
                        this.material.debugMode = value.str[0];
                    }
                },
                {
                    name: 'GrassMaps',
                    description: 'Serialized grass texture layers and blade settings',
                    type: 'object',
                    default: null,
                    options: {
                        objectTypes: [
                            JSONArray
                        ]
                    },
                    phase: 0,
                    isNullable () {
                        return true;
                    },
                    isHidden () {
                        return false;
                    },
                    get (value) {
                        const data = [];
                        const numLayers = this.grassRenderer.numLayers;
                        for(let i = 0; i < numLayers; i++){
                            const grassTexture = this.grassRenderer.getGrassTexture(i);
                            const assetId = grassTexture ? manager.getAssetId(grassTexture) ?? '' : '';
                            data.push({
                                texture: assetId,
                                bladeWidth: this.grassRenderer.getBladeWidth(i),
                                bladeHeight: this.grassRenderer.getBladeHeight(i)
                            });
                        }
                        value.object[0] = new JSONArray(null, data);
                    },
                    async set (value) {
                        const json = value.object[0];
                        const data = json?.data ?? [];
                        for(let i = 0; i < data.length; i++){
                            const info = data[i];
                            const assetId = info.texture;
                            let texture = null;
                            if (assetId) {
                                try {
                                    texture = await manager.fetchTexture(assetId);
                                } catch (err) {
                                    console.error(`Load asset failed: ${value.str[0]}: ${err}`);
                                    texture = null;
                                }
                                if (!texture?.isTexture2D()) {
                                    console.error('Invalid texture type');
                                    texture?.dispose();
                                    texture = null;
                                }
                            }
                            this.grassRenderer.addLayer(info.bladeWidth ?? 1, info.bladeHeight ?? 1, texture);
                        }
                    }
                },
                {
                    name: 'DetailMaps',
                    description: 'Serialized terrain detail texture layers',
                    type: 'object',
                    default: null,
                    options: {
                        objectTypes: [
                            JSONArray
                        ]
                    },
                    isNullable () {
                        return true;
                    },
                    isHidden () {
                        return false;
                    },
                    get (value) {
                        const data = [];
                        const material = this.material;
                        for(let i = 0; i < material.numDetailMaps; i++){
                            data.push({
                                albedo: manager.getAssetId(material.getDetailMap(i)) ?? '',
                                normal: manager.getAssetId(material.getDetailNormalMap(i)) ?? '',
                                roughness: material.getDetailMapRoughness(i),
                                uvscale: material.getDetailMapUVScale(i)
                            });
                        }
                        value.object[0] = new JSONArray(null, data);
                    },
                    async set (value) {
                        const json = value.object[0];
                        if (!json) {
                            this.material.numDetailMaps = 0;
                            return;
                        }
                        const data = json.data ?? [];
                        const material = this.material;
                        material.numDetailMaps = data.length;
                        for(let i = 0; i < this.numDetailMaps; i++){
                            const info = data[i];
                            if (!info?.albedo) {
                                material.setDetailMap(i, null);
                            } else {
                                let tex;
                                try {
                                    tex = await manager.fetchTexture(info.albedo);
                                } catch (err) {
                                    console.error(`Load asset failed: ${value.str[0]}: ${err}`);
                                    tex = null;
                                }
                                if (tex?.isTexture2D()) {
                                    material.setDetailMap(i, tex);
                                } else {
                                    console.error('Invalid texture type');
                                }
                            }
                            if (!info?.normal) {
                                material.setDetailNormalMap(i, null);
                            } else {
                                let tex;
                                try {
                                    tex = await manager.fetchTexture(info.normal);
                                } catch (err) {
                                    console.error(`Load asset failed: ${value.str[0]}: ${err}`);
                                    tex = null;
                                }
                                if (tex?.isTexture2D()) {
                                    material.setDetailMap(i, tex);
                                } else {
                                    console.error('Invalid texture type');
                                }
                            }
                            material.setDetailMapRoughness(i, info.roughness ?? 1);
                            material.setDetailMapUVScale(i, info.uvscale ?? 100);
                        }
                    }
                },
                {
                    name: 'SplatMap',
                    description: 'Serialized splat-map texture data for terrain layers',
                    type: 'embedded',
                    default: null,
                    isHidden () {
                        return true;
                    },
                    get (value) {
                        value.str[0] = this.splatMapAssetId;
                        value.object[0] = getTerrainSplatMapContent(this);
                    },
                    async set (value) {
                        if (value.str[0]) {
                            const path = value.str[0];
                            const data = await manager.VFS.readFile(path, {
                                encoding: 'binary'
                            });
                            if (!data) {
                                console.error('Load height map failed');
                                return;
                            }
                            const dataView = new DataView(data);
                            const width = dataView.getUint32(0, true);
                            const height = dataView.getUint32(4, true);
                            const numLayers = dataView.getUint32(8, true);
                            const splatMap = this.splatMap;
                            if (splatMap.width !== width || splatMap.height !== height) {
                                console.error('Invalid splatmap data');
                                return;
                            }
                            for(let i = 0; i < numLayers; i++){
                                const content = new Uint8Array(data, 3 * 4 + i * width * height * 4, width * height * 4);
                                splatMap.update(content, 0, 0, i, width, height, 1);
                            }
                            this.splatMapAssetId = value.str[0];
                        }
                    }
                },
                {
                    name: 'Grass',
                    description: 'Serialized grass instance data for the terrain',
                    type: 'embedded',
                    default: null,
                    phase: 1,
                    isHidden () {
                        return true;
                    },
                    get (value) {
                        value.str[0] = this.grassAssetId;
                        value.object[0] = getTerrainGrassContent(this);
                    },
                    async set (value) {
                        if (value.str[0]) {
                            const path = value.str[0];
                            const data = await manager.VFS.readFile(path, {
                                encoding: 'binary'
                            });
                            if (!data) {
                                console.error('Load grass data failed');
                                return;
                            }
                            const dataView = new DataView(data);
                            let offset = 0;
                            const numLayers = dataView.getUint32(offset, true);
                            if (numLayers !== this.grassRenderer.numLayers) {
                                console.error('Number of grass layers mismatch');
                                return;
                            }
                            offset += 4;
                            for(let i = 0; i < numLayers; i++){
                                const dataSize = dataView.getUint32(offset, true);
                                offset += 4;
                                if (dataSize > 0) {
                                    const data = new Float32Array(dataView.buffer, dataView.byteOffset + offset, dataSize >> 2);
                                    const numInstances = data.length >> 2;
                                    const instances = [];
                                    for(let i = 0; i < numInstances; i++){
                                        instances.push({
                                            x: data[i * 4 + 0],
                                            y: data[i * 4 + 1],
                                            angle: Math.atan2(data[i * 4 + 2], data[i * 4 + 3])
                                        });
                                    }
                                    this.grassRenderer.addInstances(i, instances);
                                    offset += dataSize;
                                }
                            }
                            this.grassAssetId = value.str[0];
                        }
                    }
                },
                {
                    name: 'HeightMap',
                    description: 'Serialized terrain height map data',
                    type: 'embedded',
                    default: null,
                    isHidden () {
                        return true;
                    },
                    get (value) {
                        value.str[0] = this.heightMapAssetId;
                        value.object[0] = getTerrainHeightMapContent(this);
                    },
                    async set (value) {
                        if (value.str[0]) {
                            const path = value.str[0];
                            const data = await manager.VFS.readFile(path, {
                                encoding: 'binary'
                            });
                            if (!data) {
                                console.error('Load height map failed');
                                return;
                            }
                            const dataView = new DataView(data);
                            const width = dataView.getUint32(0, true);
                            const height = dataView.getUint32(4, true);
                            const heightMap = this.createHeightMapTexture(width, height);
                            if (heightMap.format !== 'r16f') {
                                if (heightMap.format === 'rgba16f') {
                                    // WebGL1 uses rgba16f for height map, so we need to convert
                                    const rgbaData = new Uint16Array(width * height * 4);
                                    const rData = new Uint16Array(data, 8);
                                    for(let i = 0; i < width * height; i++){
                                        rgbaData[i * 4 + 0] = rData[i];
                                    }
                                    heightMap.update(rgbaData, 0, 0, width, height);
                                } else {
                                    throw new Error(`Unsupported height map format: ${heightMap.format}`);
                                }
                            } else {
                                heightMap.update(new Uint16Array(data, 8), 0, 0, width, height);
                            }
                            this.heightMap = heightMap;
                            this.heightMapAssetId = value.str[0];
                            this.updateBoundingBox();
                        }
                    }
                }
            ]);
        }
    };
}

export { getTerrainClass };
//# sourceMappingURL=terrain.js.map
