/* global AFRAME, THREE */

AFRAME.registerComponent('simple-navmesh-constraint', {
  schema: {
    enabled: {
      default: true
    },
    navmesh: {
      default: ''
    },
    fall: {
      default: 0.5
    },
    height: {
      default: 1.6
    },
    exclude: {
      default: ''
    },
    xzOrigin: {
      default: ''
    }
  },

  init: function () {
    this.onSceneUpdated = this.onSceneUpdated.bind(this);

    this.el.sceneEl.addEventListener('child-attached', this.onSceneUpdated);
    this.el.sceneEl.addEventListener('child-detached', this.onSceneUpdated);

    this.objects = [];
    this.excludes = [];
  },

  remove: function () {
    this.el.sceneEl.removeEventListener('child-attached', this.onSceneUpdated);
    this.el.sceneEl.removeEventListener('child-detached', this.onSceneUpdated);
  },

  onSceneUpdated: function (evt) {
    // We already have an update on the way
    if (this.entitiesChanged) { return; }

    // Don't bother updating if the entity is not relevant to us
    if (evt.detail.el.matches(this.data.navmesh) || evt.detail.el.matches(this.data.exclude)) {
      this.entitiesChanged = true;
    }
  },

  updateNavmeshEntities: function () {
    this.objects.length = 0;
    this.excludes.length = 0;

    if (this.data.navmesh.length > 0) {
      for (const navmesh of document.querySelectorAll(this.data.navmesh)) {
	this.objects.push(navmesh.object3D);
      }
    }

    if (this.objects.length === 0) {
      console.warn('simple-navmesh-constraint: Did not match any elements');
    } else if (this.data.exclude.length > 0) {
      for (const excluded of document.querySelectorAll(this.data.exclude)) {
        this.objects.push(excluded.object3D);
        this.excludes.push(excluded);
      }
    }

    this.entitiesChanged = false;
  },

  update: function () {
    this.lastPosition = null;
    this.xzOrigin = this.data.xzOrigin ? this.el.querySelector(this.data.xzOrigin) : this.el;

    this.updateNavmeshEntities();
  },

  tick: (function () {
    const nextPosition = new THREE.Vector3();
    const tempVec = new THREE.Vector3();
    const scanPattern = [
      [0,1], // Default the next location
      [0,0.5], // Check that the path to that location was fine
      [30,0.4], // A little to the side shorter range
      [-30,0.4], // A little to the side shorter range
      [60,0.2], // Moderately to the side short range
      [-60,0.2], // Moderately to the side short range
      [80,0.06], // Perpendicular very short range
      [-80,0.06], // Perpendicular very short range
    ];
    const down = new THREE.Vector3(0,-1,0);
    const raycaster = new THREE.Raycaster();
    const gravity = -1;
    const maxYVelocity = 0.5;
    const results = [];
    let yVel = 0;
    let firstTry = true;
    
    return function tick(time, delta) {
      if (this.data.enabled === false) return;
      if (this.entitiesChanged) {
	this.updateNavmeshEntities();
      }
      if (this.lastPosition === null) {
        firstTry = true;
        this.lastPosition = new THREE.Vector3();
        this.xzOrigin.object3D.getWorldPosition(this.lastPosition);
        if (this.data.xzOrigin) this.lastPosition.y -= this.xzOrigin.object3D.position.y;
      }
      
      const el = this.el;
      if (this.objects.length === 0) return;

      this.xzOrigin.object3D.getWorldPosition(nextPosition);
      if (this.data.xzOrigin) nextPosition.y -= this.xzOrigin.object3D.position.y;
      if (nextPosition.distanceTo(this.lastPosition) <= 0.01) return;
      
      let didHit = false;
      // So that it does not get stuck it takes as few samples around the user and finds the most appropriate
      scanPatternLoop:
      for (const [angle, distance] of scanPattern) {
        tempVec.subVectors(nextPosition, this.lastPosition);
        tempVec.applyAxisAngle(down, angle*Math.PI/180);
        tempVec.multiplyScalar(distance);
        tempVec.add(this.lastPosition);
        tempVec.y += maxYVelocity;
        tempVec.y -= this.data.height;
        raycaster.set(tempVec, down);
        raycaster.far = this.data.fall > 0 ? this.data.fall + maxYVelocity : Infinity;
        raycaster.intersectObjects(this.objects, true, results);
        
        if (results.length) {
          // If it hit something we want to avoid then ignore it and stop looking
          for (const result of results) {
            if(this.excludes.includes(result.object.el)) {
              results.splice(0);
              continue scanPatternLoop;
            }
          }
          const hitPos = results[0].point;
          results.splice(0);
          hitPos.y += this.data.height;
          if (nextPosition.y - (hitPos.y - yVel*2) > 0.01) {
            yVel += Math.max(gravity * delta * 0.001, -maxYVelocity);
            hitPos.y = nextPosition.y + yVel;
          } else {
            yVel = 0;
          }
          tempVec.copy(hitPos);
          this.xzOrigin.object3D.parent.worldToLocal(tempVec);
          tempVec.sub(this.xzOrigin.object3D.position);
          if (this.data.xzOrigin) tempVec.y += this.xzOrigin.object3D.position.y;
          this.el.object3D.position.add(tempVec);
          
          this.lastPosition.copy(hitPos);
          didHit = true;
          break;
        }
        
      }
      
      if (didHit) {
        firstTry = false;
      }
      
      if (!firstTry && !didHit) {
        this.el.object3D.position.copy(this.lastPosition);
        this.el.object3D.parent.worldToLocal(this.el.object3D.position);
      }
    }
  }())
});