class Enviroment{
  constructor(settings={}, game){
    this.game = game;
    // Set settings with defaults
    this.settings = Object.assign({
      floorTexture: "linoleum.png",
      wallTexture:  "wall_kachla.png",
      renderDistance: ENV_LENGTH * ENV_RENDER_DIST,
      movementSpeed: 0.075,
      movementAccel: 0.000007,
      movementStartAccel: 0.001,
      obstacles:{
        density: .5,
        frequency: .75
      }
    }, settings);

    ///// Internals
    this.movementSpeed = 0;
    this.shouldRun = false;
    this.difficulty = 0;
    this.segments = [];
    this.offset = -this.settings.renderDistance;
    this.obstacleOffset = this.offset;
    this.cache = {
      obstacles: {},
      categorized_obstacles: {
        right:  [],
        left:   [],
        mid:    [],
        dynamic:[]
      }
    };
    this.color = new THREE.Color();

    ///// Create enviroment
    this.obj = new THREE.Object3D();
    this.obj.name = "Enviroment";
    this.segmentTemplate = null;
  }

  init(){
    this.segmentTemplate = this.buildSegment();
    //this.build();
  }

  ///// Load
  async load(progress=()=>{}){
    /// Preload obstacles
    await new Promise((resolve)=>{
      var loadedObstacles = [];

      this.game.data.obstacles.forEach((obstacle)=>{
        var loadedModels = [];
        var fills = { right: false, left: false, mid: false };

        obstacle.models.forEach(async (model)=>{
          if(model.name in this.cache.obstacles) return;
          this.cache.obstacles[model.name] = (await this.game.loadModel(model.name)).scene.children[0];
          this.cache.obstacles[model.name].traverse((obj)=>{
            obj.castShadow = true;
            obj.receiveShadow = true;
            if(obj.material.map) obj.material.map.encoding = THREE.sRGBEncoding;
          });
          this.cache.obstacles[model.name].updateMatrixWorld();
          this.cache.obstacles[model.name].geometry.computeBoundingBox();
          var hitbox = this.cache.obstacles[model.name].geometry.boundingBox.clone();
          hitbox.applyMatrix4(this.cache.obstacles[model.name].matrixWorld);
          var size = hitbox.getSize(new THREE.Vector3());

          // Categorize
          if(size.x <= 1.1){
            if(model.position == "random"){
              fills.dynamic = true;
            }else{
              fills[env_xToSide(model.position[0])] = true;
            }
          }else{
            fills = { right: true, left: true, mid: true };
          }

          loadedModels.push(model.name);
          if(loadedModels.length == obstacle.models.length){
            loadedObstacles.push(obstacle.name);

            if(fills.dynamic){
              obstacle.categ = "dynamic";
              this.cache.categorized_obstacles.dynamic.push(obstacle);
            }else if(fills.right && fills.left && fills.mid){
              obstacle.categ = "wide";
            }else if(fills.right){
              obstacle.categ = "right";
              this.cache.categorized_obstacles.right.push(obstacle);
            }else if(fills.left){
              obstacle.categ = "left";
              this.cache.categorized_obstacles.left.push(obstacle);
            }else if(fills.mid){
              obstacle.categ = "mid";
              this.cache.categorized_obstacles.mid.push(obstacle);
            }
            obstacle.fills = fills;

            if(loadedObstacles.length == this.game.data.obstacles.length){
              resolve();
            }
          }
        });
      });
    });
    progress(100);
  }

  //
  //
  // TODO: use InstancedMesh corerctly
  //
  //

  ///// Returns a segment of enviroment
  buildSegment(){
    var instancedMeshes = [];
    var segment = new THREE.Object3D();
    segment.name = "EnviromentSegment";

    /// Floor
    var floorGeometry = new THREE.PlaneGeometry( ENV_WIDTH, ENV_LENGTH );
    var floorTexture = new THREE.TextureLoader().load( TEX_PATH+this.settings.floorTexture );
    floorTexture.wrapS = THREE.RepeatWrapping;
    floorTexture.wrapT = THREE.RepeatWrapping;
    floorTexture.repeat.set(ENV_WIDTH, ENV_LENGTH);
    floorTexture.encoding = THREE.sRGBEncoding;
    var floorMaterial = new THREE.MeshStandardMaterial( {color: this.color, map: floorTexture} );
    var floor = new THREE.Mesh( floorGeometry, floorMaterial, ENV_RENDER_DIST+2 );
    floor.rotation.set(-Math.PI/2, 0, 0);
    floor.position.set(0, 0, 0);
    floor.receiveShadow = this.game.settings.graphics.shadows;
    segment.add(floor);
    instancedMeshes.push(floor);

    /// Walls
    var wallGeometry = new THREE.PlaneGeometry( ENV_LENGTH, ENV_HEIGHT );
    var wallTexture = new THREE.TextureLoader().load( TEX_PATH+this.settings.wallTexture );
    wallTexture.wrapS = THREE.RepeatWrapping;
    wallTexture.wrapT = THREE.RepeatWrapping;
    wallTexture.repeat.set(ENV_WIDTH, ENV_HEIGHT);
    wallTexture.encoding = THREE.sRGBEncoding;
    var wallMaterial = new THREE.MeshStandardMaterial( {color: this.color, map: wallTexture} );
    // Wall left
    var wallL = new THREE.Mesh( wallGeometry, wallMaterial, ENV_RENDER_DIST+2 );
    wallL.rotation.set(0, -Math.PI/2, 0);
    wallL.position.set(ENV_WIDTH/2, ENV_HEIGHT/2, 0);
    wallL.receiveShadow = this.game.settings.graphics.shadows;
    segment.add(wallL);
    instancedMeshes.push(wallL);
    // Wall right
    var wallR = new THREE.Mesh( wallGeometry, wallMaterial, ENV_RENDER_DIST+2 );
    wallR.rotation.set(0, Math.PI/2, 0);
    wallR.position.set(-ENV_WIDTH/2, ENV_HEIGHT/2, 0);
    wallR.receiveShadow = this.game.settings.graphics.shadows;
    segment.add(wallR);
    instancedMeshes.push(wallR);

    //for(var i = 0; i < ENV_RENDER_DIST; i++){
      // instancedMeshes.forEach((item) => {
      //   item.setMatrixAt( i, new THREE.Matrix4().setPosition(0, 0, ENV_LENGTH*i) );
      // });


      /// Light
      if(this.game.settings.graphics.dynLights && this.game.settings.graphics.shading){
        var pointLight = new THREE.PointLight(
          0xFaFeFf,
          this.game.settings.graphics.correctLights ? 10 : 1.2,
          ENV_HEIGHT*1.5
        );
        pointLight.position.set(0, ENV_HEIGHT, /*ENV_LENGTH*i*/ + ENV_LENGTH/2);
        pointLight.castShadow = this.game.settings.graphics.shadows;
        pointLight.shadow.radius = 5;
        pointLight.shadow.mapSize.height = this.game.settings.graphics.shadowSize;
        pointLight.shadow.mapSize.width = this.game.settings.graphics.shadowSize;
        segment.add(pointLight);
      }
    //}



    return segment;
  }


  ///// Return segments close to the player
  getActiveSegments(){
    var active = [];
    this.segments.some((segment)=>{
      if(Math.abs(segment.pos - (-this.obj.position.z)) <= ENV_LENGTH){
        active.push(segment);
        return false;
      }else if(active.length > 0){
        return true;
      }
    });
    return active;
  }


  ///// Appends a segment to enviroment
  appendSegment(offset=0, obstacleFactor=1){
    var raw_pos = ENV_LENGTH + offset;
    var pos = raw_pos + this.offset;

    var object = this.segmentTemplate.clone();
    var segment = {
      object: object,
      pos: pos,
      obstacles: []
    };

    if(obstacleFactor > 0.5){
      var obstacleLength = 0;

      while (obstacleLength+ENV_OBST_LENGTH <= ENV_LENGTH){
        if(Math.random() < this.settings.obstacles.frequency*obstacleFactor){
          var addObstacle = (obstacle, side=false)=>{
            //var length = obstacle.length;
            var fills;

            obstacle.models.forEach((modelData)=>{
              var model = this.cache.obstacles[modelData.name].clone();
              /// Rotation
              if(modelData.rotation == "random") {
                model.rotation.set(
                  0,
                  Math.random()*Math.PI*2,
                  0
                );
              }else if(modelData.rotation) {
                model.rotation.set(
                  deg2rad(modelData.rotation[0]),
                  deg2rad(modelData.rotation[1]),
                  deg2rad(modelData.rotation[2])
                );
              }
              /// Position
              if(modelData.position == "random"){
                if(side){
                  model.position.set(
                    env_sideToX(side),
                    0,
                    Math.floor(Math.random() * obstacle.length)
                  );
                }else{
                  model.position.set(
                    Math.floor(ENV_WIDTH/2 - Math.floor(Math.random() * ENV_WIDTH)),
                    0,
                    Math.floor(Math.random() * obstacle.length)
                  );
                }
                fills = {};
                fills[env_xToSide(model.position.x)] = true;
              }else if (modelData.position) {
                model.position.set(
                  modelData.position[0],
                  modelData.position[1],
                  modelData.position[2]
                );
              }
              model.position.z += obstacleLength;
              /// Color
              if(modelData.color == "random"){
                model.material = model.material.clone();
                model.material.color = new THREE.Color(`hsl(${ Math.round(Math.random()*360) }, 60%, 50%)`);
              }
              //model.position.z += this.obstacleOffset - this.offset;

              //model.updateMatrixWorld(true);
              // model.geometry.computeBoundingBox();
              // var size = model.geometry.boundingBox.getSize(new THREE.Vector3());
              // if(size.z + model.position.z > length) length = size.z + model.position.z;

              segment.obstacles.push(model);
              object.add(model);
            });

            return fills || obstacle.fills;
          }

          var obstacle = this.game.data.obstacles[Math.floor(Math.random() * this.game.data.obstacles.length)];
          var filled = addObstacle(obstacle);
          var fallbackCateg = this.cache.categorized_obstacles.dynamic;
          //var length = addObstacle(obstacle);

          Object.keys(filled).forEach((side)=>{
            if((Math.random() > this.settings.obstacles.density*obstacleFactor) || filled[side]) return;

            var categ = this.cache.categorized_obstacles[side];
            var newObstacle =
              categ[Math.floor(Math.random() * categ.length)] ||
              fallbackCateg[Math.floor(Math.random() * fallbackCateg.length)]
            ;
            if(newObstacle === undefined) return;

            // var newLength = addObstacle(newObstacle);
            // if(newLength > length) length = newLength;
            addObstacle(newObstacle, side);

            filled[side] = true;
            return;
          });
        }

        obstacleLength += ENV_OBST_LENGTH;
      }
    }

    object.position.set(0, 0, pos);
    object.updateMatrixWorld(true);
    this.obj.add(object);

    this.segments.push(segment);

    this.offset += raw_pos;

    return segment;
  }


  ///// Builds enviroment to fill render distance
  build(){
    // Add first
    this.appendSegment(-ENV_LENGTH, 0);

    while(this.offset <= this.settings.renderDistance){
      this.difficulty = Math.max(0, (((this.offset / this.settings.renderDistance)+1) / 2 - 0.1));

      this.appendSegment(0, this.difficulty);
    }
  }


  ///// Start running
  run(){
    //this.movementSpeed = this.settings.movementSpeed;
    this.shouldRun = true;
  }


  ///// Reset enviroment
  reset(){
    this.movementSpeed = 0;
    this.offset = -this.settings.renderDistance;
    this.obj.position.set(0, 0, 0);
    this.segments.forEach((segment)=>{
      this.obj.remove(segment.object);
    });
    this.segments = [];
    this.segmentTemplate = null;
    this.shouldRun = false;
    this.init();
    this.build();
  }


  ///// Update physics
  update(deltaFactor){
    this.difficulty = Math.max(0.9, ((this.offset / 100) + 2) / 3 );

    // Check if there is enviroment to go into
    if(this.offset > -this.obj.position.z && this.shouldRun){
      if(this.movementSpeed < this.settings.movementSpeed){
        this.movementSpeed += this.settings.movementStartAccel;
      }

      // Move enviroment
      this.obj.position.z -= this.movementSpeed * deltaFactor;

      // Speed up movement
      if(this.movementSpeed != 0){
        this.movementSpeed += this.settings.movementAccel * deltaFactor * this.difficulty;
      }
    }


    (async ()=>{
      // Remove offscreen segments
      this.segments.some((segment, i)=>{
        if(segment.pos < -this.obj.position.z-(ENV_LENGTH*3)){
          this.obj.remove(segment.object);
          this.segments.splice(i, 1);
          return false;

        }
        return true;
      });

      // Add new segments to fill renderDistance
      while(this.offset - this.settings.renderDistance <= -this.obj.position.z){
      //for(var i = 0; i <= this.settings.renderDistance / ENV_LENGTH - (this.segments.length-1); i++){
        this.appendSegment(0, this.difficulty);
      }
    })();
  }
}

function env_sideToX(side){
  return ({mid: 0, left: 1, right: -1})[side];
}
function env_xToSide(x){
  if(x >= 1){
    return "left";
  }
  if(x <= -1){
    return "right";
  }
  return "mid";
}
