import { defined, RuntimeError } from "../../Source/Cesium.js";
import findAccessorMinMax from "../../Source/Scene/GltfPipeline/findAccessorMinMax.js";

/**
 * A fluent interface for programmatically building a glTF.
 * @alias GltfBuilder
 * @constructor
 * @private
 */
function GltfBuilder() {
  this.gltf = {
    asset: {
      generator: "cesium-tests",
      version: "2.0",
    },
    extensionsUsed: [],
    accessors: [],
    buffers: [],
    bufferViews: [],
    materials: [],
    meshes: [],
    nodes: [
      {
        mesh: 0,
      },
    ],
    scenes: [
      {
        nodes: [0],
      },
    ],
    scene: 0,
  };

  this.bufferBuilders = [];
}

/**
 * Creates a new buffer.
 * @param {string} [name] The name of the buffer.
 * @returns {GltfBufferBuilder}
 */
GltfBuilder.prototype.buffer = function (name) {
  var index =
    this.gltf.buffers.push({
      name: name,
      byteLength: 0,
    }) - 1;
  var bufferBuilder = new GltfBufferBuilder(this, index);
  this.bufferBuilders.push(bufferBuilder);
  return bufferBuilder;
};

/**
 * Creates a new mesh.
 * @param {string} [name] The name of the mesh.
 * @returns {GltfMeshBuilder}
 */
GltfBuilder.prototype.mesh = function (name) {
  var index =
    this.gltf.meshes.push({
      name: name,
      primitives: [],
    }) - 1;

  var meshBuilder = new GltfMeshBuilder(this, index);
  return meshBuilder;
};

/**
 * Creates a new material.
 * @param {string} [name] The name of the material.
 * @returns {GltfMaterialBuilder}
 */
GltfBuilder.prototype.material = function (name) {
  var index =
    this.gltf.materials.push({
      name: name,
    }) - 1;
  var materialBuilder = new GltfMaterialBuilder(this, index);
  return materialBuilder;
};

/**
 * Gets the built glTF JSON from this builder. Calling this a second
 * time will cause the glTF returned in the first call to be invalidated.
 * Specifically, the `uri` properties of its buffers will no longer be
 * resolvable.
 *
 * After calling this method, be sure to call {@link GltfBuilder#destroy}
 * when done with the glTF, to a void leaking buffer memory.
 */
GltfBuilder.prototype.toGltf = function () {
  for (var i = 0; i < this.bufferBuilders.length; ++i) {
    var bufferBuilder = this.bufferBuilders[i];

    var byteLength = bufferBuilder.viewBuilders.reduce(function (
      byteLength,
      viewBuilder
    ) {
      return byteLength + viewBuilder.bufferView.byteLength;
    },
    0);

    var buffer = new ArrayBuffer(byteLength);
    var nextStart = 0;

    for (var j = 0; j < bufferBuilder.viewBuilders.length; ++j) {
      var viewBuilder = bufferBuilder.viewBuilders[j];
      viewBuilder.bufferView.byteOffset = nextStart;
      var destBuffer =
        viewBuilder.componentType === 5126
          ? new Float32Array(buffer, nextStart, viewBuilder._data.length)
          : new Uint16Array(buffer, nextStart, viewBuilder._data.length);
      destBuffer.set(viewBuilder._data);
      nextStart += viewBuilder.bufferView.byteLength;
    }

    bufferBuilder.buffer.byteLength = byteLength;

    if (bufferBuilder.buffer.uri) {
      URL.revokeObjectURL(bufferBuilder.buffer.uri);
    }

    bufferBuilder.buffer.uri = URL.createObjectURL(new Blob([buffer]));

    bufferBuilder.buffer.extras = {
      _pipeline: {
        source: new Uint8Array(buffer, 0, buffer.byteLength),
      },
    };
  }

  var gltf = this.gltf;
  gltf.accessors.forEach(function (accessor) {
    var minMax = findAccessorMinMax(gltf, accessor);
    accessor.min = minMax.min;
    accessor.max = minMax.max;
  });

  this.bufferBuilders.forEach(function (builder) {
    delete builder.buffer.extras;
  });

  return this.gltf;
};

/**
 * Frees memory allocated for buffers in {@link GltfBuilder@toGltf}.
 */
GltfBuilder.prototype.destroy = function () {
  this.bufferBuilders.forEach(function (bufferBuilder) {
    URL.revokeObjectURL(bufferBuilder.buffer.uri);
    bufferBuilder.buffer.uri = undefined;
  });
};

/**
 * A fluent interface for programmatically building a glTF material.
 * @param {GltfBuilder} gltfBuilder The glTF builder.
 * @param {number} materialIndex The index of this material within the glTF.
 * @private
 */
function GltfMaterialBuilder(gltfBuilder, materialIndex) {
  this.gltfBuilder = gltfBuilder;
  this.materialIndex = materialIndex;
  this.material = this.gltfBuilder.gltf.materials[materialIndex];
}

/**
 * Defines the material using JSON.
 * @param {*} json The JSON definition of the material.
 * @returns {GltfMaterialBuilder}
 */
GltfMaterialBuilder.prototype.json = function (json) {
  for (var property in json) {
    if (!json.hasOwnProperty(property)) {
      continue;
    }

    this.material[property] = json[property];
  }

  return this;
};

/**
 * A fluent interface for building a glTF mesh.
 * @param {GltfBuilder} gltfBuilder The glTF builder.
 * @param {number} meshIndex The index of this mesh within the glTF.
 * @private
 */
function GltfMeshBuilder(gltfBuilder, meshIndex) {
  this.gltfBuilder = gltfBuilder;
  this.meshIndex = meshIndex;
  this.mesh = gltfBuilder.gltf.meshes[this.meshIndex];
}

/**
 * Creates a new primitive within this mesh.
 * @param {string} [name] The name of the primitive.
 * @returns {GltfPrimitiveBuilder}
 */
GltfMeshBuilder.prototype.primitive = function (name) {
  var index =
    this.mesh.primitives.push({
      name: name,
      attributes: {},
    }) - 1;

  var meshBuilder = new GltfPrimitiveBuilder(this, index);
  return meshBuilder;
};

/**
 * A fluent interface for building a glTF primitive.
 * @param {GltfMeshBuilder} gltfMeshBuilder The mesh builder.
 * @param {number} primitiveIndex The index of this primitive within the mesh.
 * @private
 */
function GltfPrimitiveBuilder(gltfMeshBuilder, primitiveIndex) {
  this.gltfMeshBuilder = gltfMeshBuilder;
  this.primitiveIndex = primitiveIndex;
  this.primitive = this.gltfMeshBuilder.mesh.primitives[this.primitiveIndex];
}

/**
 * Adds a new attribute to the primitive.
 * @param {string} semantic The semantic of the attribute.
 * @param {string} accessorName The name of the accessor referenced by this attribute.
 * @returns {GltfPrimitiveBuilder}
 */
GltfPrimitiveBuilder.prototype.attribute = function (semantic, accessorName) {
  var gltf = this.gltfMeshBuilder.gltfBuilder.gltf;
  var accessorId = findAccessorByName(gltf, accessorName);
  if (accessorId < 0) {
    throw new RuntimeError("Accessor named " + accessorName + " not found.");
  }

  this.primitive.attributes[semantic] = accessorId;
  return this;
};

/**
 * Sets the name of the accessor providing this primitive's indices.
 * @param {string} accessorName The name of the accessor providing the indices.
 * @returns {GltfPrimitiveBuilder}
 */
GltfPrimitiveBuilder.prototype.indices = function (accessorName) {
  var gltf = this.gltfMeshBuilder.gltfBuilder.gltf;
  var accessorId = findAccessorByName(gltf, accessorName);
  if (accessorId < 0) {
    throw new RuntimeError("Accessor named " + accessorName + " not found.");
  }

  this.primitive.indices = accessorId;
  return this;
};

/**
 * Indicates that this primitive is TRIANGLES.
 * @returns {GltfPrimitiveBuilder}
 */
GltfPrimitiveBuilder.prototype.triangles = function () {
  this.primitive.mode = 4;
  return this;
};

/**
 * Sets the material applied to this primitive.
 * @param {string} materialName The name of the material.
 * @returns {GltfPrimitiveBuilder}
 */
GltfPrimitiveBuilder.prototype.material = function (materialName) {
  var gltf = this.gltfMeshBuilder.gltfBuilder.gltf;

  for (var i = 0; i < gltf.materials.length; ++i) {
    if (gltf.materials[i].name === materialName) {
      this.primitive.material = i;
      return this;
    }
  }

  throw new RuntimeError("Material named " + materialName + " not found.");
};

/**
 * A fluent interface for building a glTF buffer.
 * @param {GltfBuilder} gltfBuilder The glTF builder.
 * @param {number} bufferIndex The index of this buffer in the glTF.
 * @private
 */
function GltfBufferBuilder(gltfBuilder, bufferIndex) {
  this.gltfBuilder = gltfBuilder;
  this.bufferIndex = bufferIndex;
  this.buffer = this.gltfBuilder.gltf.buffers[bufferIndex];
  this.viewBuilders = [];
}

/**
 * Creates a new vertex bufferView in this buffer.
 * @param {string} name The name of the bufferView.
 * @returns {GltfBufferViewBuilder}
 */
GltfBufferBuilder.prototype.vertexBuffer = function (name) {
  var index =
    this.gltfBuilder.gltf.bufferViews.push({
      name: name,
      buffer: this.bufferIndex,
      byteLength: 0,
      target: 34962,
    }) - 1;
  var viewBuilder = new GltfBufferViewBuilder(this, index, 5126);
  this.viewBuilders.push(viewBuilder);
  return viewBuilder;
};

/**
 * Creates a new index bufferView in this buffer.
 * @param {string} name The name of the bufferView.
 * @returns {GltfBufferViewBuilder}
 */
GltfBufferBuilder.prototype.indexBuffer = function (name) {
  var index =
    this.gltfBuilder.gltf.bufferViews.push({
      name: name,
      buffer: this.bufferIndex,
      byteLength: 0,
      target: 34963,
    }) - 1;
  var viewBuilder = new GltfBufferViewBuilder(this, index, 5123);
  this.viewBuilders.push(viewBuilder);
  return viewBuilder;
};

/**
 * A fluent interface for building a glTF bufferView.
 * @param {GltfBufferBuilder} bufferBuilder The buffer builder.
 * @param {GltfBufferViewBuilder} bufferViewIndex The bufferView builder.
 * @param {number} componentType The glTF `componentType` of this bufferView.
 * @private
 */
function GltfBufferViewBuilder(bufferBuilder, bufferViewIndex, componentType) {
  this.bufferBuilder = bufferBuilder;
  this.bufferViewIndex = bufferViewIndex;
  this.bufferView = this.bufferBuilder.gltfBuilder.gltf.bufferViews[
    this.bufferViewIndex
  ];
  this.componentType = componentType;
  this.elementStride = 0;
  this.nextOffset = 0;
  this._data = undefined;
}

Object.defineProperties(GltfBufferViewBuilder.prototype, {
  /**
   * Gets the number of bytes in each numeric element of this bufferView.
   */
  elementByteLength: {
    get: function () {
      return this.componentType === 5126 ? 4 : 2;
    },
  },
});

/**
 * Defines a `VEC3` element in this bufferView.
 * @param {string} name The name of the accessor for this element.
 * @returns {GltfBufferViewBuilder}
 */
GltfBufferViewBuilder.prototype.vec3 = function (name) {
  var gltf = this.bufferBuilder.gltfBuilder.gltf;
  gltf.accessors.push({
    name: name,
    bufferView: this.bufferViewIndex,
    byteOffset: this.nextOffset,
    componentType: this.componentType,
    count: 0,
    type: "VEC3",
  });

  this.elementStride += 3;

  var newStride = 3 * this.elementByteLength;
  if (!defined(this.bufferView.byteStride)) {
    this.bufferView.byteStride = newStride;
  } else {
    this.bufferView.byteStride += newStride;
  }

  this.nextOffset += newStride;

  return this;
};

/**
 * Defines a `SCALAR` element in this bufferView.
 * @param {string} name The name of the accessor for this element.
 * @returns {GltfBufferViewBuilder}
 */
GltfBufferViewBuilder.prototype.scalar = function (name) {
  var gltf = this.bufferBuilder.gltfBuilder.gltf;
  gltf.accessors.push({
    name: name,
    bufferView: this.bufferViewIndex,
    byteOffset: this.nextOffset,
    componentType: this.componentType,
    count: 0,
    type: "SCALAR",
  });

  this.elementStride += 1;

  var newStride = this.elementByteLength;
  if (!defined(this.bufferView.byteStride)) {
    this.bufferView.byteStride = newStride;
  } else {
    this.bufferView.byteStride += newStride;
  }

  this.nextOffset += newStride;

  return this;
};

/**
 * Provides this bufferView's data. All elements should be defined with
 * {@link GltfBufferViewBuilder#vec3} and {@link GltfBufferViewBuilder#scalar}
 * before calling this method.
 * @param {Array|Float32Array|Uint16Array|Uint32Array} data The data.
 * @returns {GltfBufferViewBuilder}
 */
GltfBufferViewBuilder.prototype.data = function (data) {
  this.bufferView.byteLength = data.length * this.elementByteLength;
  this._data = data;

  var count = data.length / this.elementStride;

  var gltf = this.bufferBuilder.gltfBuilder.gltf;
  var bufferViewIndex = this.bufferViewIndex;

  gltf.accessors
    .filter(function (accessor) {
      return accessor.bufferView === bufferViewIndex;
    })
    .forEach(function (accessor) {
      accessor.count = count;
    });

  return this;
};

function findAccessorByName(gltf, accessorName) {
  var accessors = gltf.accessors;

  for (var i = 0; i < accessors.length; ++i) {
    if (accessors[i].name === accessorName) {
      return i;
    }
  }

  return -1;
}

export default GltfBuilder;
