"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getModelLoader = void 0;
const core_1 = require("@babylonjs/core");
const deepmerge_1 = __importDefault(require("deepmerge"));
const utils_1 = require("../utils");
const parser_1 = require("./parser");
const types_1 = require("./types");
const TINT_COLOR = new core_1.Color4(145 / 255, 189 / 255, 89 / 255, 1);
const WATER_COLOR = new core_1.Color4(36 / 255, 57 / 255, 214 / 255, 1);
const LAVA_COLOR = new core_1.Color4(232 / 255, 89 / 255, 23 / 255, 1);
const AMBIENT_LIGHT = new core_1.Color3(0.5, 0.5, 0.5);
const DEG2RAD = Math.PI / 180;
function normalize(input) {
    return input / 16 - 0.5;
}
function getModelLoader(resourceLoader) {
    const materialCache = new Map();
    const modelCache = new Map();
    const clearCache = () => {
        materialCache.clear();
        modelCache.clear();
    };
    function getTextureMaterial(tex, scene, rotation, transparent) {
        return __awaiter(this, void 0, void 0, function* () {
            // Normalise values for better caching.
            if (rotation === 0) {
                rotation = undefined;
            }
            const cacheKey = `${tex}_rot=${rotation}`;
            // TODO - Determine if there's a better way to handle this other than manually caching.
            const cached = materialCache.get(cacheKey);
            if (cached) {
                return cached;
            }
            if (tex.startsWith('minecraft:')) {
                tex = tex.substring('minecraft:'.length);
            }
            const blob = yield resourceLoader.getResourceBlob(`textures/${tex}.png`);
            if (blob === undefined) {
                console.log(tex);
                return undefined;
            }
            const texture = new core_1.Texture(blob, scene, false, true, core_1.Texture.NEAREST_NEAREST_MIPNEAREST);
            texture.hasAlpha = transparent;
            texture.isBlocking = false;
            if (rotation) {
                texture.wAng = rotation * DEG2RAD;
            }
            const mat = new core_1.StandardMaterial(cacheKey, scene);
            mat.diffuseTexture = texture;
            mat.useAlphaFromDiffuseTexture = transparent;
            mat.ambientColor = AMBIENT_LIGHT;
            mat.disableDepthWrite = transparent;
            materialCache.set(cacheKey, mat);
            return mat;
        });
    }
    function getBlockModelData(block, blockState) {
        var _a;
        const models = [];
        const validVariantProperties = blockState.variants
            ? new Set(Object.keys(blockState.variants)[0]
                .split(',')
                .map(a => a.split('=')[0]))
            : new Set(Object.keys(block.properties));
        const variantName = Object.keys(block.properties)
            .sort()
            .reduce((a, b) => {
            if (!validVariantProperties.has(b)) {
                return a;
            }
            a.push(`${b}=${block.properties[b]}`);
            return a;
        }, [])
            .join(',');
        const createWeightedModels = (model) => {
            if (Array.isArray(model)) {
                return model.map(m => { var _a; return ({ holder: m, weight: (_a = m.weight) !== null && _a !== void 0 ? _a : 1 }); });
            }
            return [{ holder: model, weight: 1 }];
        };
        if ((_a = blockState.variants) === null || _a === void 0 ? void 0 : _a['']) {
            models.push({
                options: createWeightedModels(blockState.variants['']),
            });
        }
        else if (blockState.variants) {
            models.push({
                options: createWeightedModels(blockState.variants[variantName]),
            });
        }
        else if (blockState.multipart) {
            const doesFilterPass = (filter) => {
                for (const property of Object.keys(filter)) {
                    if (block.properties[property] !== filter[property]) {
                        return false;
                    }
                }
                return true;
            };
            for (const part of blockState.multipart) {
                if (part.when) {
                    // Check filters
                    if (part.when.OR) {
                        let anyPassed = false;
                        for (const test of part.when.OR) {
                            if (doesFilterPass(test)) {
                                anyPassed = true;
                                break;
                            }
                        }
                        if (!anyPassed) {
                            continue;
                        }
                    }
                    else {
                        if (!doesFilterPass(part.when)) {
                            continue;
                        }
                    }
                }
                models.push({ options: createWeightedModels(part.apply) });
            }
        }
        const name = variantName.length > 0
            ? `${block.type}[${variantName}]`
            : block.type;
        return { models, name };
    }
    const getModelOption = (data) => {
        const weightedRandomIndex = (options) => {
            const weights = [];
            for (let i = 0; i < options.length; i++) {
                weights[i] = options[i].weight + (weights[i - 1] || 0);
            }
            const random = Math.random() * weights[weights.length - 1];
            for (let i = 0; i < weights.length; i++) {
                if (weights[i] > random) {
                    return i;
                }
            }
            return weights.length - 1;
        };
        let name = data.name;
        const holders = [];
        for (const model of data.models) {
            const index = weightedRandomIndex(model.options);
            holders.push(model.options[index].holder);
            name = `${name}-${index}`;
        }
        return { name, holders };
    };
    const getModel = (data, block, scene) => __awaiter(this, void 0, void 0, function* () {
        if (modelCache.has(data.name)) {
            return modelCache
                .get(data.name)
                .map((mesh, i) => mesh.createInstance(`${data.name}-${i}`));
        }
        const group = [];
        for (let modelIndex = 0; modelIndex < data.holders.length; modelIndex++) {
            const modelHolder = data.holders[modelIndex];
            const model = yield (0, parser_1.loadModel)(modelHolder.model, resourceLoader);
            const resolveTexture = (ref) => {
                while (ref.startsWith('#')) {
                    ref = model.textures[ref.substring(1)];
                }
                return ref;
            };
            if (block.type === 'water' || block.type === 'lava') {
                // These blocks are not rendered via models, so handle specially.
                model.textures['all'] = model.textures.particle;
                const temporaryModel = (0, deepmerge_1.default)(yield (0, parser_1.loadModel)('block/cube_all', resourceLoader), model);
                model.textures = temporaryModel.textures;
                model.elements = temporaryModel.elements;
            }
            if (model.elements) {
                for (const element of model.elements) {
                    if (Object.keys(element.faces).length === 0) {
                        continue;
                    }
                    // Rewrite bottom to down for weird MC quirk.
                    if (element.faces['bottom']) {
                        element.faces['down'] = element.faces['bottom'];
                    }
                    // Normalize to/from to local coords.
                    element.from = element.from.map(normalize);
                    element.to = element.to.map(normalize);
                    if (element.rotation) {
                        element.rotation.origin = element.rotation.origin.map(normalize);
                    }
                    const elementSize = [
                        element.to[0] - element.from[0],
                        element.to[1] - element.from[1],
                        element.to[2] - element.from[2],
                    ];
                    const colours = [];
                    const uvs = [];
                    let hasColor = false;
                    let subMaterials = [];
                    for (const face of types_1.POSSIBLE_FACES) {
                        const faceData = element.faces[face];
                        if (!faceData) {
                            subMaterials.push(undefined);
                            colours.push(undefined);
                            uvs.push(undefined);
                            continue;
                        }
                        const tex = resolveTexture(faceData.texture);
                        subMaterials.push(yield getTextureMaterial(tex, scene, faceData.rotation, utils_1.TRANSPARENT_BLOCKS.has(block.type) ||
                            faceData.texture.includes('overlay')));
                        let color;
                        if (faceData.tintindex !== undefined) {
                            color = TINT_COLOR;
                        }
                        else if (tex.startsWith('block/water_')) {
                            color = WATER_COLOR;
                        }
                        else if (tex.startsWith('block/lava_')) {
                            color = LAVA_COLOR;
                        }
                        else {
                            colours.push(undefined);
                            continue;
                        }
                        hasColor = true;
                        colours.push(color);
                        if (!faceData.uv) {
                            faceData.uv = [0, 0, 16, 16];
                        }
                        faceData.uv = faceData.uv.map(u => u / 16);
                        uvs.push(new core_1.Vector4(...faceData.uv));
                    }
                    const box = core_1.MeshBuilder.CreateBox(`${data.name}-${modelIndex}`, {
                        width: elementSize[0] || 0.001,
                        height: elementSize[1] || 0.001,
                        depth: elementSize[2] || 0.001,
                        wrap: true,
                        faceColors: hasColor ? colours : undefined,
                        updatable: false,
                        faceUV: uvs,
                    }, scene);
                    box.doNotSyncBoundingInfo = true;
                    const subMeshes = [];
                    const verticesCount = box.getTotalVertices();
                    for (let i = 0; i < types_1.POSSIBLE_FACES.length; i++) {
                        if (!subMaterials[i]) {
                            continue;
                        }
                        // Only create the submesh if it has a material.
                        const subMesh = new core_1.SubMesh(subMeshes.length, i, verticesCount, i * 6, 6, box, undefined, true, false);
                        subMeshes.push(subMesh);
                    }
                    box.subMeshes = subMeshes;
                    // Remove the undefined ones.
                    subMaterials = subMaterials.filter(mat => mat);
                    // If we're a list of the same material, just use that.
                    if (subMaterials.length > 1) {
                        const testMaterial = subMaterials[0];
                        let allMatch = true;
                        for (let i = 1; i < subMaterials.length; i++) {
                            if (subMaterials[i] !== testMaterial) {
                                allMatch = false;
                                break;
                            }
                        }
                        if (allMatch) {
                            subMaterials = [testMaterial];
                        }
                    }
                    let material = undefined;
                    if (subMaterials.length > 1) {
                        material = new core_1.MultiMaterial(`${data.name}-${modelIndex}`, scene);
                        material.subMaterials = subMaterials;
                    }
                    else if (subMaterials.length === 1) {
                        material = subMaterials[0];
                    }
                    else {
                        continue;
                    }
                    box.material = material;
                    if (element.rotation) {
                        box.setPivotPoint(new core_1.Vector3(element.rotation.origin[0], element.rotation.origin[1], element.rotation.origin[2]), core_1.Space.WORLD);
                        switch (element.rotation.axis) {
                            case 'y':
                                box.rotate(core_1.Axis.Y, element.rotation.angle * -DEG2RAD, core_1.Space.WORLD);
                                break;
                            case 'x':
                                box.rotate(core_1.Axis.X, element.rotation.angle * DEG2RAD, core_1.Space.WORLD);
                                break;
                            case 'z':
                                box.rotate(core_1.Axis.Z, element.rotation.angle * DEG2RAD, core_1.Space.WORLD);
                                break;
                        }
                        box.setPivotPoint(new core_1.Vector3(0, 0, 0));
                    }
                    if (modelHolder.x) {
                        box.rotate(core_1.Axis.X, -DEG2RAD * modelHolder.x, core_1.Space.WORLD);
                    }
                    if (modelHolder.y) {
                        box.rotate(core_1.Axis.Y, -DEG2RAD * modelHolder.y, core_1.Space.WORLD);
                    }
                    box.translate(core_1.Axis.X, element.from[0] + elementSize[0] / 2)
                        .translate(core_1.Axis.Y, element.from[1] + elementSize[1] / 2)
                        .translate(core_1.Axis.Z, element.from[2] + elementSize[2] / 2);
                    group.push(box);
                }
            }
        }
        // We cannot merge these meshes as we lose independent face colours.
        for (const mesh of group) {
            mesh.setEnabled(false);
            mesh.isVisible = false;
        }
        modelCache.set(data.name, group);
        return group.map((mesh, i) => mesh.createInstance(`${data.name}-${i}`));
    });
    return {
        clearCache,
        getBlockModelData,
        getModelOption,
        getModel,
    };
}
exports.getModelLoader = getModelLoader;
//# sourceMappingURL=loader.js.map