import DPR from 'dpr';
import './utils/threeLoader';
import './utils/vectorThreeEnhancement';
import {CADTrackballControls} from './controls/CADTrackballControls';
import {
  AmbientLight,
  Box3,
  DirectionalLight,
  Euler,
  Matrix4,
  Object3D,
  OrthographicCamera,
  PerspectiveCamera,
  Raycaster,
  Scene,
  Vector3,
  WebGLRenderer
} from "three";
import {Emitter, stream} from "lstream";
import {Camera} from "three/src/cameras/Camera";

export default class SceneSetUp {
  workingSphere: number;
  container: HTMLElement;
  scene: Scene;
  rootGroup: Object3D;
  oCamera: OrthographicCamera;
  pCamera: PerspectiveCamera;
  camera: Camera;
  light: DirectionalLight;
  renderer: WebGLRenderer;
  private _prevContainerWidth: number;
  private _prevContainerHeight: number;
  trackballControls: CADTrackballControls;
  viewportSizeUpdate$ = stream();
  sceneRendered$: Emitter<any> = stream();

  renderRequested: boolean;

  constructor(container) {
    
    this.workingSphere = 10000;
    this.container = container;
    this.scene = new Scene();
    this.rootGroup = this.scene;
    this.scene.userData.sceneSetUp = this;
    this.renderRequested = false;

    this.setUpCamerasAndLights();
    this.setUpControls();

    this.animate();
  }

  aspect() {
    return this.container.clientWidth / this.container.clientHeight;
  }

  requestRender() {
    this.renderRequested = true;
  }

  createOrthographicCamera() {
    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    const factor = ORTHOGRAPHIC_CAMERA_FACTOR;
    this.oCamera = new OrthographicCamera(-width / factor,
      width / factor,
      height / factor,
      -height / factor, 0.1, 1000000);
    this.oCamera.position.z = 1000;
    this.oCamera.position.x = -1000;
    this.oCamera.position.y = 300;
  }

  createPerspectiveCamera() {
    this.pCamera = new PerspectiveCamera( 60, this.aspect(), 0.1, 1000000 );
    this.pCamera.position.z = 1000;
    this.pCamera.position.x = -1000;
    this.pCamera.position.y = 300;
  }

  setUpCamerasAndLights() {
    this.createOrthographicCamera();
    this.createPerspectiveCamera();

    this.camera = this.pCamera;
    
    this.light = new DirectionalLight( 0xffffff );
    this.light.position.set( 10, 10, 10 );
    this.scene.add(this.light);

    this.scene.add( new AmbientLight( 0xffffff, 0.25 ) );

    this.renderer = new WebGLRenderer();
    this.renderer.setPixelRatio(DPR);
    this.renderer.setClearColor(0x808080, 1);
    this.renderer.setSize( this.container.clientWidth,  this.container.clientHeight );
    this.container.appendChild( this.renderer.domElement );
  }
  
  updateViewportSize() {
    if (this.container.clientWidth > 0 && this.container.clientHeight > 0) {
      this.updatePerspectiveCameraViewport();
      this.updateOrthographicCameraViewport();
      this.renderer.setSize( this.container.clientWidth, this.container.clientHeight );
      this.viewportSizeUpdate$.next();
      this.__render_NeverCallMeFromOutside();
    }
  }

  updateViewportSizeIfNeeded() {
    if (this._prevContainerWidth !== this.container.clientWidth || 
        this._prevContainerHeight !== this.container.clientHeight) {
      this.updateViewportSize();
      this._prevContainerWidth = this.container.clientWidth;
      this._prevContainerHeight = this.container.clientHeight;
    }
  }

  updatePerspectiveCameraViewport() {
    this.pCamera.aspect = this.aspect();
    this.pCamera.updateProjectionMatrix();
  }

  updateOrthographicCameraViewport() {
    const width = this.container.clientWidth;
    const height = this.container.clientHeight;
    const factor = ORTHOGRAPHIC_CAMERA_FACTOR;
    this.oCamera.left = - width / factor;
    this.oCamera.right = width / factor;
    this.oCamera.top = height / factor;
    this.oCamera.bottom = - height / factor;
    this.oCamera.updateProjectionMatrix();
  }

  syncCameras(sourceCamera, targetCamera) {
    const camPosition = new Vector3();
    const camRotation = new Euler();
    const tempMatrix = new Matrix4();

    camPosition.setFromMatrixPosition( targetCamera.matrixWorld );
    camRotation.setFromRotationMatrix( tempMatrix.extractRotation( targetCamera.matrixWorld ) );
    const camDistance = sourceCamera.position.length();

    sourceCamera.up.copy(this.camera.up);
    sourceCamera.position.copy(camPosition);
    sourceCamera.quaternion.copy(camPosition);
    sourceCamera.position.normalize();
    sourceCamera.position.multiplyScalar(camDistance);
  }

  setCamera(camera) {
    this.syncCameras(camera, this.camera);
    this.camera = camera;
    this.trackballControls.setCameraMode(camera.isOrthographicCamera);
    this.trackballControls.object = camera;
    this.requestRender();
  }

  setUpControls() {
    //  controls = new THREE.OrbitControls( camera , renderer.domElement);
    const trackballControls: any = new CADTrackballControls(this.camera , this.renderer.domElement);

    // document.addEventListener( 'mousemove', function(){

    //   controls.update();

    // }, false );
    trackballControls.rotateSpeed = 3.8 * DPR;
    trackballControls.projectionZoomSpeed = 0.5 * DPR;
    trackballControls.zoomSpeed = 1.2 * DPR;
    trackballControls.panSpeed = 0.3 * DPR;

    trackballControls.noZoom = false;
    trackballControls.noPan = false;

    trackballControls.staticMoving = true;
    trackballControls.dynamicDampingFactor = 0.3;

    trackballControls.keys = [ 65, 83, 68 ];
    this.trackballControls = trackballControls;
  }

  createRaycaster(viewX, viewY) {
    const raycaster = new Raycaster();
    raycaster.params.Line.threshold = 12 * (this._zoomMeasure() * 0.8);

    (raycaster.params as any).Line2 = {
      threshold: 20
    };

    const x = ( viewX / this.container.clientWidth ) * 2 - 1;
    const y = - ( viewY / this.container.clientHeight ) * 2 + 1;

    const mouse = new Vector3( x, y, 1 );
    raycaster.setFromCamera( mouse, this.camera );
    return raycaster;
  }
  
  raycast(event, objects, logInfoOut = null) {
    const raycaster = this.createRaycaster(event.offsetX, event.offsetY);
    if (logInfoOut !== null) {
      logInfoOut.ray = raycaster.ray
    }

    const intersects = [];

    function intersectObject(object) {

      object.raycast( raycaster, intersects );

      const children = object.children;

      if (object.visible) {
        for ( let i = 0, l = children.length; i < l; i ++ ) {
          intersectObject(children[ i ]);
        }
      }
    }

    objects.forEach(intersectObject);

    intersects.sort((a, b) => {
      if (Math.abs(a.distance - b.distance) < 0.01 && (a.object.raycastPriority || b.object.raycastPriority)) {
        return b.object.raycastPriority||0 - a.object.raycastPriority||0;
      }
      return a.distance - b.distance;
    })
    return intersects;
  }

  customRaycast(from3, to3, objects) {
    const raycaster = new Raycaster();
    const from = new Vector3().fromArray(from3);
    const to = new Vector3().fromArray(to3);
    const dir = to.sub(from);
    const dist = dir.length();
    raycaster.set(from, dir.normalize());
    return raycaster.intersectObjects(objects, true ).filter(h => h.distance <= dist);
  }
  
  modelToScreen(pos) {
    const width = this.container.clientWidth, height = this.container.clientHeight;
    const widthHalf = width / 2, heightHalf = height / 2;

    const vector = new Vector3();
    vector.copy(pos);
    vector.project(this.camera);

    vector.x = ( vector.x * widthHalf ) + widthHalf;
    vector.y = - ( vector.y * heightHalf ) + heightHalf;
    return vector;
  }
  
  lookAtObject(obj) {
    const camera = this.camera;

    const box = new Box3();
    box.setFromObject(obj);
    const size = box.getSize(new Vector3());
    //this.camera.position.set(0,0,0);

    box.getCenter(camera.position);
    const maxSize = Math.max(size.x, size.z);
    camera.position.addScaledVector(camera.position.normalize(), 5000);
    //this.camera.position.sub(new THREE.Vector3(0, 0, dist));
    camera.up = new Vector3(0, 1, 0);
  }

  _zoomMeasure() {
    return this.trackballControls.object.position.length() / 1e3;
  }
  
  animate() {
    requestAnimationFrame( () => this.animate() );
    const controlsChangedViewpoint = this.trackballControls.evaluate();
    if (controlsChangedViewpoint || this.renderRequested) {
      this.__render_NeverCallMeFromOutside();
    }
    this.updateViewportSizeIfNeeded();
  }

  private __render_NeverCallMeFromOutside() {
    this.renderRequested = false;
    this.light.position.set(this.camera.position.x, this.camera.position.y, this.camera.position.z);
    this.renderer.render(this.scene, this.camera);
    this.sceneRendered$.next();
  }

  domElement() {
    return this.renderer.domElement;   
  }
}

export function getSceneSetup(object3D) {
  do {
    if (object3D.userData.sceneSetUp) {
      return object3D.userData.sceneSetUp;
    }
    object3D = object3D.parent;
  } while(object3D);
  return null;
}

const ORTHOGRAPHIC_CAMERA_FACTOR = 1;