'use strict';
const Cesium = require('cesium');
const obj2gltf = require('../../lib/obj2gltf');
const createGltf = require('../../lib/createGltf');
const loadObj = require('../../lib/loadObj');
const { getDefaultMaterial } = require('../../lib/loadMtl');

const clone = Cesium.clone;
const defined = Cesium.defined;
const WebGLConstants = Cesium.WebGLConstants;

const boxObjPath = 'specs/data/box/box.obj';
const groupObjPath = 'specs/data/box-objects-groups-materials/box-objects-groups-materials.obj';
const complexObjPath = 'specs/data/box-complex-material/box-complex-material.obj';
const noMaterialsObjPath = 'specs/data/box-no-materials/box-no-materials.obj';
const mixedAttributesObjPath = 'specs/data/box-mixed-attributes-2/box-mixed-attributes-2.obj';

let options;

describe('createGltf', () => {
    let boxObjData;
    let groupObjData;
    let complexObjData;
    let noMaterialsObjData;
    let mixedAttributesObjData;

    beforeEach(async () => {
        options = clone(obj2gltf.defaults);
        options.overridingTextures = {};
        options.logger = () => {};

        boxObjData = await loadObj(boxObjPath, options);
        groupObjData = await loadObj(groupObjPath, options);
        complexObjData = await loadObj(complexObjPath, options);
        noMaterialsObjData = await loadObj(noMaterialsObjPath, options);
        mixedAttributesObjData = await loadObj(mixedAttributesObjPath, options);
    });

    it('simple gltf', () => {
        const gltf = createGltf(boxObjData, options);

        expect(gltf.materials.length).toBe(1);
        expect(gltf.scene).toBe(0);
        expect(gltf.scenes[0].nodes[0]).toBe(0);
        expect(gltf.nodes.length).toBe(1);
        expect(gltf.meshes.length).toBe(1);

        const primitives = gltf.meshes[0].primitives;
        const primitive = primitives[0];
        const attributes = primitive.attributes;
        const positionAccessor = gltf.accessors[attributes.POSITION];
        const normalAccessor = gltf.accessors[attributes.NORMAL];
        const uvAccessor = gltf.accessors[attributes.TEXCOORD_0];
        const indexAccessor = gltf.accessors[primitive.indices];

        expect(primitives.length).toBe(1);
        expect(positionAccessor.count).toBe(24);
        expect(normalAccessor.count).toBe(24);
        expect(uvAccessor.count).toBe(24);
        expect(indexAccessor.count).toBe(36);
    });

    it('does not combine buffers when that buffer would exceed the Node buffer size limit', () => {
        spyOn(createGltf, '_getBufferMaxByteLength').and.returnValue(0);
        const clonedOptions = clone(options, true);
        clonedOptions.separate = true;

        const gltf = createGltf(boxObjData, clonedOptions);
        expect(gltf.accessors.length).toBe(4);
        expect(gltf.buffers.length).toBe(4);
        expect(gltf.bufferViews.length).toBe(4);

        const length = gltf.buffers.length;
        for (let i = 0; i < length; ++i) {
            const accessor = gltf.accessors[i];
            const bufferView = gltf.bufferViews[i];
            const buffer = gltf.buffers[i];
            expect(accessor.bufferView).toBe(i);
            expect(accessor.byteOffset).toBe(0);
            expect(bufferView.buffer).toBe(i);
            expect(bufferView.byteOffset).toBe(0);
            expect(bufferView.byteLength).toBe(buffer.byteLength);
        }
    });

    it('multiple nodes, meshes, and primitives', () => {
        const gltf = createGltf(groupObjData, options);

        expect(gltf.materials.length).toBe(3);
        expect(gltf.scene).toBe(0);
        expect(gltf.scenes[0].nodes[0]).toBe(0);
        expect(gltf.nodes.length).toBe(4);
        expect(gltf.nodes[0].mesh).toBeUndefined();
        expect(gltf.nodes[0].children.length).toBe(3);
        expect(gltf.meshes.length).toBe(3);

        // Check for two primitives in each mesh
        const length = gltf.meshes.length;
        for (let i = 0; i < length; ++i) {
            const mesh = gltf.meshes[i];
            expect(mesh.primitives.length).toBe(2);
        }
    });

    it('multiple textures', () => {
        const gltf = createGltf(complexObjData, options);
        const material = gltf.materials[0];
        const pbr = material.pbrMetallicRoughness;
        const textures = [pbr.metallicRoughnessTexture, pbr.baseColorTexture, material.emissiveTexture, material.normalTexture, material.occlusionTexture];
        expect(textures.map((texture) => {
            return texture.index;
        }).sort()).toEqual([0, 1, 2, 3, 4]);
        expect(gltf.samplers[0]).toBeDefined();
    });

    it('creates default material', () => {
        const gltf = createGltf(noMaterialsObjData, options);
        const material = gltf.materials[0];
        const pbr = material.pbrMetallicRoughness;
        expect(material.name).toBe('default');
        expect(pbr.baseColorTexture).toBeUndefined();
        expect(pbr.metallicRoughnessTexture).toBeUndefined();
        expect(pbr.baseColorFactor).toEqual([0.5, 0.5, 0.5, 1.0]);
        expect(pbr.metallicFactor).toBe(0.0); // No metallic
        expect(pbr.roughnessFactor).toBe(1.0); // Fully rough
        expect(material.emissiveTexture).toBeUndefined();
        expect(material.normalTexture).toBeUndefined();
        expect(material.ambientTexture).toBeUndefined();
        expect(material.emissiveFactor).toEqual([0.0, 0.0, 0.0]);
        expect(material.alphaMode).toBe('OPAQUE');
        expect(material.doubleSided).toBe(false);
    });

    it('adds KHR_materials_pbrSpecularGlossiness extension when specularGlossiness is set', () => {
        options.specularGlossiness = true;
        const gltf = createGltf(noMaterialsObjData, options);
        expect(gltf.extensionsUsed).toEqual(['KHR_materials_pbrSpecularGlossiness']);
        expect(gltf.extensionsRequired).toEqual(['KHR_materials_pbrSpecularGlossiness']);
    });

    it('adds KHR_materials_unlit extension when unlit is set', () => {
        options.unlit = true;
        const gltf = createGltf(noMaterialsObjData, options);
        expect(gltf.extensionsUsed).toEqual(['KHR_materials_unlit']);
        expect(gltf.extensionsRequired).toEqual(['KHR_materials_unlit']);
    });

    it('runs without normals', () => {
        boxObjData.nodes[0].meshes[0].primitives[0].normals.length = 0;

        const gltf = createGltf(boxObjData, options);
        const attributes = gltf.meshes[0].primitives[0].attributes;
        expect(attributes.POSITION).toBeDefined();
        expect(attributes.NORMAL).toBeUndefined();
        expect(attributes.TEXCOORD_0).toBeDefined();
    });

    it('runs without uvs', () => {
        boxObjData.nodes[0].meshes[0].primitives[0].uvs.length = 0;

        const gltf = createGltf(boxObjData, options);
        const attributes = gltf.meshes[0].primitives[0].attributes;
        expect(attributes.POSITION).toBeDefined();
        expect(attributes.NORMAL).toBeDefined();
        expect(attributes.TEXCOORD_0).toBeUndefined();
    });

    it('runs without uvs and normals', () => {
        boxObjData.nodes[0].meshes[0].primitives[0].normals.length = 0;
        boxObjData.nodes[0].meshes[0].primitives[0].uvs.length = 0;

        const gltf = createGltf(boxObjData, options);
        const attributes = gltf.meshes[0].primitives[0].attributes;
        expect(attributes.POSITION).toBeDefined();
        expect(attributes.NORMAL).toBeUndefined();
        expect(attributes.TEXCOORD_0).toBeUndefined();
    });

    it('splits incompatible materials', () => {
        const gltf = createGltf(mixedAttributesObjData, options);
        const materials = gltf.materials;
        const meshes = gltf.meshes;

        const referenceMaterial = mixedAttributesObjData.materials[0];
        delete referenceMaterial.name;
        referenceMaterial.pbrMetallicRoughness.baseColorTexture = {
            index : 0
        };

        const referenceMaterialNoTextures = clone(referenceMaterial, true);
        referenceMaterialNoTextures.pbrMetallicRoughness.baseColorTexture = undefined;

        const defaultMaterial = getDefaultMaterial(options);
        delete defaultMaterial.name;

        const materialNames = materials.map((material) => {
            const name = material.name;
            delete material.name;
            return name;
        });

        // Expect three copies of each material for
        // * positions/normals/uvs
        // * positions/normals
        // * positions/uvs
        expect(materialNames).toEqual([
            'default',
            'default-2',
            'default-3',
            'Material',
            'Material-2',
            'Material-3',
            'Missing',
            'Missing-2',
            'Missing-3'
        ]);

        expect(materials.length).toBe(9);
        expect(materials[0]).toEqual(defaultMaterial);
        expect(materials[1]).toEqual(defaultMaterial);
        expect(materials[2]).toEqual(defaultMaterial);
        expect(materials[3]).toEqual(referenceMaterial);
        expect(materials[4]).toEqual(referenceMaterial);
        expect(materials[5]).toEqual(referenceMaterialNoTextures);
        expect(materials[6]).toEqual(defaultMaterial);
        expect(materials[7]).toEqual(defaultMaterial);
        expect(materials[8]).toEqual(defaultMaterial);

        // Test that primitives without uvs reference materials without textures
        const meshesLength = meshes.length;
        for (let i = 0; i < meshesLength; ++i) {
            const mesh = meshes[i];
            const primitives = mesh.primitives;
            const primitivesLength = primitives.length;
            for (let j = 0; j < primitivesLength; ++j) {
                const primitive = primitives[j];
                const material = materials[primitive.material];
                if (!defined(primitive.attributes.TEXCOORD_0)) {
                    expect(material.pbrMetallicRoughness.baseColorTexture).toBeUndefined();
                }
            }
        }
    });

    function expandObjData(objData, duplicatesLength) {
        const primitive = objData.nodes[0].meshes[0].primitives[0];
        const indices = primitive.indices;
        const positions = primitive.positions;
        const normals = primitive.normals;
        const uvs = primitive.uvs;

        const indicesLength = indices.length;
        const vertexCount = positions.length / 3;

        for (let i = 1; i < duplicatesLength; ++i) {
            for (let j = 0; j < vertexCount; ++j) {
                positions.push(0.0);
                positions.push(0.0);
                positions.push(0.0);
                normals.push(0.0);
                normals.push(0.0);
                normals.push(0.0);
                uvs.push(0.0);
                uvs.push(0.0);
            }
            for (let k = 0; k < indicesLength; ++k) {
                indices.push(indices.get(k) + vertexCount * i);
            }
        }
    }

    it('detects need to use uint32 indices', () => {
        expandObjData(boxObjData, 2731); // Right above 65536 limit
        let primitive = boxObjData.nodes[0].meshes[0].primitives[0];
        const indicesLength = primitive.indices.length;
        const vertexCount = primitive.positions.length / 3;

        const gltf = createGltf(boxObjData, options);
        primitive = gltf.meshes[0].primitives[0];
        const indicesAccessor = gltf.accessors[primitive.indices];
        expect(indicesAccessor.count).toBe(indicesLength);
        expect(indicesAccessor.max[0]).toBe(vertexCount - 1);
        expect(indicesAccessor.componentType).toBe(WebGLConstants.UNSIGNED_INT);

        const positionAccessor = gltf.accessors[primitive.attributes.POSITION];
        expect(positionAccessor.count).toBe(vertexCount);
    });
});