const GLTFCache = {};

// From https://gist.github.com/cdata/f2d7a6ccdec071839bc1954c32595e87
// Tracking glTF cloning here: https://github.com/mrdoob/three.js/issues/11573
function cloneGltf(gltf) {
  const clone = {
    animations: gltf.animations,
    scene: gltf.scene.clone(true)
  };

  const skinnedMeshes = {};

  gltf.scene.traverse(node => {
    if (node.isSkinnedMesh) {
      skinnedMeshes[node.name] = node;
    }
  });

  const cloneBones = {};
  const cloneSkinnedMeshes = {};

  clone.scene.traverse(node => {
    if (node.isBone) {
      cloneBones[node.name] = node;
    }

    if (node.isSkinnedMesh) {
      cloneSkinnedMeshes[node.name] = node;
    }
  });

  for (const name in skinnedMeshes) {
    const skinnedMesh = skinnedMeshes[name];
    const skeleton = skinnedMesh.skeleton;
    const cloneSkinnedMesh = cloneSkinnedMeshes[name];

    const orderedCloneBones = [];

    for (let i = 0; i < skeleton.bones.length; ++i) {
      const cloneBone = cloneBones[skeleton.bones[i].name];
      orderedCloneBones.push(cloneBone);
    }

    cloneSkinnedMesh.bind(new THREE.Skeleton(orderedCloneBones, skeleton.boneInverses), cloneSkinnedMesh.matrixWorld);

    cloneSkinnedMesh.material = skinnedMesh.material.clone();
  }

  return clone;
}

const inflateEntities = function(classPrefix, parentEl, node) {
  // setObject3D mutates the node's parent, so we have to copy
  const children = node.children.slice(0);

  const el = document.createElement("a-entity");

  // Remove invalid CSS class name characters.
  const className = node.name.replace(/[^\w-]/g, "");
  el.classList.add(classPrefix + className);
  parentEl.appendChild(el);

  // Copy over transform to the THREE.Group and reset the actual transform of the Object3D
  el.setAttribute("position", {
    x: node.position.x,
    y: node.position.y,
    z: node.position.z
  });
  el.setAttribute("rotation", {
    x: node.rotation.x * THREE.Math.RAD2DEG,
    y: node.rotation.y * THREE.Math.RAD2DEG,
    z: node.rotation.z * THREE.Math.RAD2DEG
  });
  el.setAttribute("scale", {
    x: node.scale.x,
    y: node.scale.y,
    z: node.scale.z
  });

  node.position.set(0, 0, 0);
  node.rotation.set(0, 0, 0);
  node.scale.set(1, 1, 1);

  el.setObject3D(node.type.toLowerCase(), node);

  children.forEach(childNode => {
    inflateEntities(classPrefix, el, childNode);
  });
};

function attachTemplate(templateEl) {
  const selector = templateEl.getAttribute("data-selector");
  const targetEls = templateEl.parentNode.querySelectorAll(selector);
  const clone = document.importNode(templateEl.content, true);
  const templateRoot = clone.firstElementChild;
  const templateRootAttrs = templateRoot.attributes;

  for (var i = 0; i < targetEls.length; i++) {
    const targetEl = targetEls[i];

    // Merge root element attributes with the target element
    for (var i = 0; i < templateRootAttrs.length; i++) {
      targetEl.setAttribute(templateRootAttrs[i].name, templateRootAttrs[i].value);
    }

    // Append all child elements
    for (var i = 0; i < templateRoot.children.length; i++) {
      targetEl.appendChild(document.importNode(templateRoot.children[i], true));
    }
  }
}

AFRAME.registerElement("a-gltf-entity", {
  prototype: Object.create(AFRAME.AEntity.prototype, {
    load: {
      value() {
        if (this.hasLoaded || !this.parentEl) {
          return;
        }

        // Get the src url.
        let src = this.getAttribute("src");

        // If the src attribute is a selector, get the url from the asset item.
        if (src.charAt(0) === "#") {
          const assetEl = document.getElementById(src.substring(1));
          src = assetEl.getAttribute("src");
        }

        const onLoad = gltfModel => {
          if (!GLTFCache[src]) {
            // Store a cloned copy of the gltf model.
            GLTFCache[src] = cloneGltf(gltfModel);
          }

          this.model = gltfModel.scene || gltfModel.scenes[0];
          this.model.animations = gltfModel.animations;

          this.setObject3D("mesh", this.model);
          this.emit("model-loaded", { format: "gltf", model: this.model });

          if (this.getAttribute("inflate")) {
            inflate(this.model, finalizeLoad);
          } else {
            finalizeLoad();
          }
        };

        const inflate = (model, callback) => {
          inflateEntities("", this, model);
          this.querySelectorAll(":scope > template").forEach(attachTemplate);

          // Wait one tick for the appended custom elements to be connected before calling finalizeLoad
          setTimeout(callback, 0);
        };

        const finalizeLoad = () => {
          AFRAME.ANode.prototype.load.call(this, () => {
            // Check if entity was detached while it was waiting to load.
            if (!this.parentEl) {
              return;
            }

            this.updateComponents();
            if (this.isScene || this.parentEl.isPlaying) {
              this.play();
            }
          });
        };

        // Load the gltf model from the cache if it exists.
        const gltf = GLTFCache[src];

        if (gltf) {
          // Use a cloned copy of the cached model.
          const clonedGltf = cloneGltf(gltf);
          onLoad(clonedGltf);
          return;
        }

        // Otherwise load the new gltf model.
        new THREE.GLTFLoader().load(src, onLoad, undefined /* onProgress */, error => {
          // On glTF load error

          const message = error && error.message ? error.message : "Failed to load glTF model";
          console.warn(message);
          this.emit("model-error", { format: "gltf", src });
        });
      }
    }
  })
});