// Copyright 2017 The Draco Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
'use_strict';

const fs = require('fs');
const assert = require('assert');
const draco3d = require('draco3d');

// Global decoder and encoder module variables.
let decoderModule = null;
let encoderModule = null;

// The code to create the encoder and decoder modules is asynchronous.
// draco3d.createDecoderModule will return a promise to a funciton with a
// module as a parameter when the module has been fully initialized.
// Create and set the decoder module.
draco3d.createDecoderModule({}).then(function(module) {
  // This is reached when everything is ready, and you can call methods on
  // Module.
  decoderModule = module;
  console.log('Decoder Module Initialized!');
  modulesInitialized();
});

// Create and set the encoder module.
draco3d.createEncoderModule({}).then(function(module) {
  // This is reached when everything is ready, and you can call methods on
  // Module.
  encoderModule = module;
  console.log('Encoder Module Initialized!');
  modulesInitialized();
});

function modulesInitialized() {
  // Check if both the encoder and decoder modules have been initialized.
  if (encoderModule && decoderModule) {
    fs.readFile('./bunny.drc', function(err, data) {
      if (err) {
        return console.log(err);
      }
      console.log("Decoding file of size " + data.byteLength + " ..");
      // Decode mesh
      const decoder = new decoderModule.Decoder();
      const decodedGeometry = decodeDracoData(data, decoder);
      // Encode mesh
      encodeMeshToFile(decodedGeometry, decoder);

      decoderModule.destroy(decoder);
      decoderModule.destroy(decodedGeometry);
    });
  }
}

function decodeDracoData(rawBuffer, decoder) {
  const buffer = new decoderModule.DecoderBuffer();
  buffer.Init(new Int8Array(rawBuffer), rawBuffer.byteLength);
  const geometryType = decoder.GetEncodedGeometryType(buffer);

  let dracoGeometry;
  let status;
  if (geometryType === decoderModule.TRIANGULAR_MESH) {
    dracoGeometry = new decoderModule.Mesh();
    status = decoder.DecodeBufferToMesh(buffer, dracoGeometry);
  } else if (geometryType === decoderModule.POINT_CLOUD) {
    dracoGeometry = new decoderModule.PointCloud();
    status = decoder.DecodeBufferToPointCloud(buffer, dracoGeometry);
  } else {
    const errorMsg = 'Error: Unknown geometry type.';
    console.error(errorMsg);
  }
  decoderModule.destroy(buffer);

  return dracoGeometry;
}

function encodeMeshToFile(mesh, decoder) {
  const encoder = new encoderModule.Encoder();
  const meshBuilder = new encoderModule.MeshBuilder();
  // Create a mesh object for storing mesh data.
  const newMesh = new encoderModule.Mesh();

  const numFaces = mesh.num_faces();
  const numIndices = numFaces * 3;
  const numPoints = mesh.num_points();
  const indices = new Uint32Array(numIndices);

  console.log("Number of faces " + numFaces);
  console.log("Number of vertices " + numPoints);

  // Add Faces to mesh
  const ia = new decoderModule.DracoInt32Array();
  for (let i = 0; i < numFaces; ++i) {
    decoder.GetFaceFromMesh(mesh, i, ia);
    const index = i * 3;
    indices[index] = ia.GetValue(0);
    indices[index + 1] = ia.GetValue(1);
    indices[index + 2] = ia.GetValue(2);
  }
  decoderModule.destroy(ia);
  meshBuilder.AddFacesToMesh(newMesh, numFaces, indices);

  const attrs = {POSITION: 3, NORMAL: 3, COLOR: 3, TEX_COORD: 2};

  Object.keys(attrs).forEach((attr) => {
    const stride = attrs[attr];
    const numValues = numPoints * stride;
    const decoderAttr = decoderModule[attr];
    const encoderAttr = encoderModule[attr];
    const attrId = decoder.GetAttributeId(mesh, decoderAttr);

    if (attrId < 0) {
      return;
    }

    console.log("Adding %s attribute", attr);

    const attribute = decoder.GetAttribute(mesh, attrId);
    const attributeData = new decoderModule.DracoFloat32Array();
    decoder.GetAttributeFloatForAllPoints(mesh, attribute, attributeData);

    assert(numValues === attributeData.size(), 'Wrong attribute size.');

    const attributeDataArray = new Float32Array(numValues);
    for (let i = 0; i < numValues; ++i) {
      attributeDataArray[i] = attributeData.GetValue(i);
    }

    decoderModule.destroy(attributeData);
    meshBuilder.AddFloatAttributeToMesh(newMesh, encoderAttr, numPoints,
        stride, attributeDataArray);
  });

  let encodedData = new encoderModule.DracoInt8Array();
  // Set encoding options.
  encoder.SetSpeedOptions(5, 5);
  encoder.SetAttributeQuantization(encoderModule.POSITION, 10);
  encoder.SetEncodingMethod(encoderModule.MESH_EDGEBREAKER_ENCODING);

  // Encoding.
  console.log("Encoding...");
  const encodedLen = encoder.EncodeMeshToDracoBuffer(newMesh,
                                                     encodedData);
  encoderModule.destroy(newMesh);

  if (encodedLen > 0) {
    console.log("Encoded size is " + encodedLen);
  } else {
    console.log("Error: Encoding failed.");
  }
  // Copy encoded data to buffer.
  const outputBuffer = new ArrayBuffer(encodedLen);
  const outputData = new Int8Array(outputBuffer);
  for (let i = 0; i < encodedLen; ++i) {
    outputData[i] = encodedData.GetValue(i);
  }
  encoderModule.destroy(encodedData);
  encoderModule.destroy(encoder);
  encoderModule.destroy(meshBuilder);
  // Write to file. You can view the the file using webgl_loader_draco.html
  // example.
  fs.writeFile("bunny_10.drc", Buffer.from(outputBuffer), "binary",
               function(err) {
    if (err) {
        console.log(err);
    } else {
        console.log("The file was saved!");
    }
  });
}