
/// <reference path="FDParserFactory.ts" />
/// <reference path="FDVisualisationFactory.ts" />
/// <reference path="FDEventHandler.ts" />
/// <reference path="resources/tween.js.d.ts" />



module FD {


    export class FluidDiagrams {

        private forceRenderer: String;
        debug: Boolean = true;
        private parser: FDParser;
        private visualisation: FDVisualisation;
        private eventHandler: FDEventHandler;
        public width: number;
        public height: number;
        private container;
        public nodes: FDNode[];
        public rootNodes: FDNode[];
        public camera: THREE.Camera;
        private lights: THREE.Light[];
        public domElement: HTMLCanvasElement;
        private NodeMeshMap: { [index: string]: FDNode; } = {};
        public nodeSceneGraph: THREE.Object3D;
        public geometryObjects: THREE.Mesh[];
        private trackBallControls = null;
        private keepAspectRatio: boolean;
        private aspectRatio: number;
        private scale: boolean;
        private renderer: THREE.Renderer;
        public scene: THREE.Scene;
        private parserType: string;
        private parserFactory: FDParserFactory;
        private visualisationType: string;
        private visualisationFactory: FDVisualisationFactory;


        /**
        * Creates a new instance of FluidDiagrams
        * @class FluidDiagrams
        * @classdesc Fluid Diagrams Main Class.
        * @author Benedict Wright
        * @param {bool} [debug]  
        * @param {String} [forceRenderer]. WebGL(Default), SVG(Not good), Canvas
        */
        constructor(debug: Boolean = true, forceRenderer: String = "WebGL") {
            this.debug = debug;
            debugPrinter.print("init", this.debug);
            this.parserFactory = new FDParserFactory(debug);
            debugPrinter.print("init parser factory", this.debug);
            this.visualisationFactory = new FDVisualisationFactory(debug, this);
            debugPrinter.print("init vis factory", this.debug);
            
            this.lights = [];
            this.geometryObjects = [];
            this.nodeSceneGraph = new THREE.Object3D();
            debugPrinter.print("init scenegraph", this.debug);
            this.forceRenderer = forceRenderer;
            this.parserType = "";
            this.visualisationType = "";
            
        }

        /**
        * Sets dimension of the visualisation
        * @memberof FluidDiagrams
        * @function setDimesions
        * @param {int} width 
        * @param {int} height
        */
        public setDimensions(width: number, height: number, scale: boolean= false, keepAspectRatio: boolean = true) {
            this.width = width;
            this.height = height;
            this.keepAspectRatio = keepAspectRatio;
            this.scale = scale;
            if (this.keepAspectRatio == true) {
                this.aspectRatio = height / width;
            }
        }

        /**
        * Sets the containing HTML element
        * @memberof FluidDiagrams
        * @function setContainer
        * @param {string} container 
        */
        public setContainer(container: string) {
            this.container = <any>$(container);
        }

        /**
        * Sets the parser type
        * @memberof FluidDiagrams
        * @function setParserType
        * @param {string} pType 
        */
        public setParserType(pType: string) {
            this.parserType = pType;
            this.parser = this.parserFactory.getParser(pType);
        }
        /**
        * Returns the parser
        * @memberof FluidDiagrams
        * @function getParser
        */
        public getParser(): FDParser {
            if (this.parser == null) {
                if (this.parserType == "")
                    return null;
                this.parser = this.parserFactory.getParser(this.parserType);
                return this.parser;
            } else {
                return this.parser;
            }
        }

         /**
        * Sets the visualisation type
        * @memberof FluidDiagrams
        * @function setVisualisationType
        * @param {string} vType 
        */
        public setVisualisationType(vType: string) {
            this.visualisationType = vType;
            this.visualisation = this.visualisationFactory.getVisualisation(vType);
        }
         /**
        * Returns the visualisation
        * @memberof FluidDiagrams
        * @function getVisualisation
        */
        public getVisualisation(): FDVisualisation {
            if (this.visualisation == null) {
                if (this.visualisationType == "")
                    return null;
                this.visualisation = this.visualisationFactory.getVisualisation(this.visualisationType);
                return this.visualisation;
            } else {
                return this.visualisation;
            }
        }

        /**
        * Sets the Camera for the Scene
        * @memberof FluidDiagrams
        * @function setCamera
        * @param {THREE.Camera} camera
        */
        public setCamera(camera: THREE.Camera) {
            this.camera = camera;
        }
        /**
        * Returns the camera
        * @memberof FluidDiagrams
        * @function getCamera
        * @returns {THREE.Camera}
        */
        public getCamera(): THREE.Camera {
            return this.camera;
        }

        public activateTrackballControls() {
            this.trackBallControls = new (<any>THREE).TrackballControls(this.camera, this.domElement);

            this.trackBallControls.addEventListener('change', this.renderer);
        }

        /**
        * Sets the event handler
        * @memberof FluidDiagrams
        * @function setEventHandler
        * @param {FDEventHandler} eventHander
        */
        public setEventHandler(eventHander: FDEventHandler) {
            this.eventHandler = eventHander;
        }

        public getEventHandler(){
            return this.eventHandler;
        }        

        /**
        * Adds a light source to the scene
        * @memberof FluidDiagrams
        * @function addLight
        * @param {THREE.Light} light
        */
        public addLight(light: THREE.Light) {
            this.lights.push(light);
        }

        /**
        * executes the parser, executes the visualisation algorithm, calls createScene()
        * @memberof FluidDiagrams
        * @function run
        */
        public run() {

            this.height = this.container.height();
            this.width = this.container.width();
            debugPrinter.print(this.width + " " + this.height, this.debug);

            this.parser.parse();
            this.nodes = this.parser.getAllNodes();
            this.rootNodes = this.parser.getRootNodes();

            this.visualisation.setDimensions(this.width, this.height);
            this.visualisation.setNodes(this.rootNodes);
            this.visualisation.setAllNodes(this.nodes);
            this.visualisation.visualise();

            //print to WEBGL
            this.createScene();
            //debugPrinter.print("created scene no Links", this.debug);

        }

        /**
        * calls the eventhandlers update method, gets called in every frame of the visualisation
        * @memberof FluidDiagrams
        * @function updateScene
        */
        private updateScene() {
            if (this.eventHandler != undefined) {
                this.eventHandler.update();
            }
            this.renderer.render(this.scene, this.camera);
            //if (this.trackBallControls != null) {
            //    this.trackBallControls.update();
            //}

        }


        /**
        * creates the render enginge (Canvas or WebGL). Adds the Nodes from the Parser to the scene. Adds all Scene Elements. And initialises the main loop
        * @memberof FluidDiagrams
        * @function createScene
        */
        private createScene() {

            var container = this.container
            this.scene = new THREE.Scene();


            if (this.forceRenderer == "Canvas") {
                this.renderer = new THREE.CanvasRenderer();
            }
            if (this.forceRenderer == "SVG") {
                this.renderer = new (<any>(THREE)).SVGRenderer()
        }

            if (this.forceRenderer != "SVG" && this.forceRenderer != "Canvas") {
                if (!(<any>window).WebGLRenderingContext) {
                    //NO WEBGL
                    this.renderer = new THREE.CanvasRenderer();
                    //alert("canvas Renderer decativated. ");
                    debugPrinter.print("canvas rederer: using Software Acceleration!!SLOW", this.debug);
                } else {
                    //WEBGL
                    try {
                        var context = (<any>document.createElement('canvas')).getContext('experimental-webgl');
                        if (context) {
                            this.renderer = new THREE.WebGLRenderer({ antialias: true });
                            debugPrinter.print("WebGL Renderer", this.debug);
                        } else {
                            this.renderer = new THREE.CanvasRenderer();
                            //alert("canvas Renderer decativated. ");
                            debugPrinter.print("canvas rederer: using Software Acceleration!!SLOW", this.debug);

                        }
                    } catch (e) {

                        debugPrinter.print("some error occured during init of WebGL,retry canvas: " + e.message, this.debug);
                        this.renderer = new THREE.CanvasRenderer();
                        return;
                    }
                }
            }




            (<any >this.renderer).setSize(this.width, this.height);
            (<any>this.renderer).shadowMapEnabled = true;
            (<any>this.renderer).shadowMapSoft = true;
            (<any>this.renderer).setClearColor(new THREE.Color(this.visualisation.backgroundColor));
            this.scene.add(this.camera);

            if (this.debug) {
                 var axes = new THREE.AxisHelper(20);
                 axes.position=new THREE.Vector3(2, 2, 1);
                 this.scene.add(axes);

            }

            for (var node in this.rootNodes) {

                this.scene.add(this.rootNodes[node].getNodeSceneGraph());
                //debugPrinter.print("adding: " + this.nodes[node].getUniqueId()+" at:"+this.nodes[node].getPosition().x+"/"+this.nodes[node].getPosition().y+"/"+this.nodes[node].getPosition().z, this.debug);
                //adding node to Map for later retrieval
            }

            //mesh ids to Node
            for (var node in this.nodes) {
                //debugPrinter.print(this.nodes[node].getUniqueId()+"has ids: ",this.debug);
                var ids = this.nodes[node].getMeshIds();
                for (var id in ids) {
                    this.NodeMeshMap[ids[id]] = this.nodes[node];
                    //debugPrinter.print(ids[id],this.debug);
                }

                //debugPrinter.print("node: "+this.nodes[node].getUniqueId()+"length: " + this.nodes[node].getAllGeometry().length, this.debug);
            }
            //debugPrinter.print("added nodes", this.debug);


            for (var l in this.lights) {
                //debugPrinter.print("adding lights", this.debug);
                this.scene.add(this.lights[l]);
            }
            //debugPrinter.print("added lights", this.debug);

            this.scene.add(this.nodeSceneGraph);



            container.append((<any>this.renderer).domElement);
            //debugPrinter.print("appended to container", this.debug);
            if (this.eventHandler != undefined) {
                this.addEventsHandlers();
            }

            this.mainloop();


        }

        /**
        * the visualisations main loop, calls the TWEEN.update function for all TWEEN animations. calls updateScene.
        * Requests a new animation Frame and calls itsself recursivly
        * @memberof FluidDiagrams
        * @function mainloop
        */
        private mainloop() {
            var self = this;

            //debugPrinter.print("updateScene", this.debug);
            (<any>TWEEN).update();
            this.updateScene();
            //debugPrinter.print("request new animation frame", this.debug);
            requestAnimationFrame(function () { self.mainloop() });
        }

        /**
        * Adds eventlisteners to the DOM element
        * @memberof FluidDiagrams
        * @function addEventsHandlers
        */
        private addEventsHandlers() {

            this.domElement = (<THREE.WebGLRenderer>this.renderer).domElement;
            var that = this;
            //add many more event listeners
            this.domElement.addEventListener('mouseup', function (e) { that.onMouseUp(e); }, false);
            this.domElement.addEventListener('mousedown', function (e) { that.onMouseDown(e); }, false);
            this.domElement.addEventListener('mousemove', function (e) { that.onMouseMove(e); }, false);
            this.domElement.addEventListener('click', function (e) { that.onMouseClick(e); }, false);
            //IE9, Chrome, Sfari, Opera
            this.domElement.addEventListener("mousewheel", function (e) { that.onMouseWheel(e); }, false);
            //Firefox
            this.domElement.addEventListener("DOMMouseScroll", function (e) { that.onMouseWheel(e); }, false);

            this.domElement.addEventListener("mouseout", function (e) { that.onMouseOut(e); }, false);
            window.addEventListener("resize", function (e) { that.onWindowResize(e); }, false);

        }

        private onMouseOut(e: Event) {
            this.onMouseUp(e);
        }

        private onMouseUp(e: Event) {
            e.preventDefault();
            this.eventHandler.onMouseUp(e);
        }

        private onMouseClick(e: Event) {
            e.preventDefault();
            this.eventHandler.onMouseClick(e, this.objectFromMouse((<any>e).clientX, (<any>e).clientY));
        }

        private onMouseDown(e: Event) {
            e.preventDefault();
            this.eventHandler.onMouseDown(e);
        }

        private onMouseMove(e: Event) {
            e.preventDefault();
            this.eventHandler.onMouseMove(e);
        }

        private onMouseWheel(e: Event) {
            e.preventDefault();
            this.eventHandler.mouseWheel(e);
        }


        /**
        * Casts a ray in positiv z direction from the camera to find objects at pagex/pagey coordinates (Converted to local space)
        * If hit object is a FDNode the node is returned else the mesh that was hit or null is returned
        * @memberof FluidDiagrams
        * @function objectFromMouse
        * @param {int} pagex x coordinates in page space
        * @param {int} pagey y coordinates in page space
        * @return {FDNode | THREE.Mesh}
        */
        public objectFromMouse(pagex: number, pagey: number) {

            //translate page coordinates to element coordinates
 
            var offsetLeft =this.domElement.getBoundingClientRect().left;
            var offsetTop = this.domElement.getBoundingClientRect().top;
            var eltx = pagex - offsetLeft;
            var elty = pagey - offsetTop;

            //debugPrinter.print("offset x/y:"+offsetLeft+"/"+offsetTop+ "  pageXY: "+pagex+"/"+pagey, true);

            var vpx = ((eltx / this.domElement.width) * 2 - 1);
            var vpy = (-(elty / this.domElement.height) * 2 + 1);


          
            if (this.camera instanceof THREE.PerspectiveCamera || (<THREE.CombinedCamera>this.camera).inPerspectiveMode) {
                var vector = new THREE.Vector3(vpx, vpy, 0.5);
                var projector = new THREE.Projector();
                projector.unprojectVector(vector, this.camera);
                var direction = vector.sub(this.camera.position).normalize();
                var ray = new THREE.Raycaster(this.camera.position, direction);
                var intersects = (<any>ray).intersectObjects(this.scene.children, true);

                if (intersects.length > 0) {
                    var i = 0;
                    while (!intersects[i].object.visible) {
                        i++;
                    }
                    var intersected = <THREE.Mesh>intersects[i].object;

                    var clickedNode = this.NodeMeshMap[intersected.geometry.id.toString()];

                    if (clickedNode != null) {//node was clicked
                        return <any>clickedNode;
                    }
                    //sceneObject was clicked
                    return <any> intersected;
                }
            } else if (this.camera instanceof THREE.OrthographicCamera) {
                var projector = new THREE.Projector();
                var ray = projector.pickingRay(new THREE.Vector3(vpx, vpy, 0.5), this.camera);

                var intersects = (<any>ray).intersectObjects(this.scene.children, true);


                if (intersects.length > 0) {
                    var visibleElement = 0;
                    for (var i = 0; i < intersects.length; i++) {
                        if (intersects[i].object.visible) {
                            visibleElement = i;
                        }
                    }
                    var intersected = <THREE.Mesh>intersects[visibleElement].object;
                    var clickedNode = this.NodeMeshMap[intersected.geometry.id.toString()];

                    if (clickedNode != null) {//node was c licked
                        return <any>clickedNode;
                    }
                    //sceneObject was clicked
                    return <any> intersected;
                }
            } else if ((<THREE.CombinedCamera>this.camera).inOrthographicMode) {
                var projector = new THREE.Projector();
                var ray = projector.pickingRay(new THREE.Vector3(vpx, vpy, 0.5), this.camera);

                var intersects = (<any>ray).intersectObjects(this.scene.children, true);


                if (intersects.length > 0) {
                    var visibleElement = 0;
                    var i = 0;
                    while (!intersects[i].object.visible) {
                        i++;
                    }

                    var intersected = <THREE.Mesh>intersects[i].object;
                    var clickedNode = this.NodeMeshMap[intersected.geometry.id.toString()];

                    if (clickedNode != null) {//node was c licked
                        return <any>clickedNode;
                    }
                    //sceneObject was clicked
                    return <any> intersected;
                }
            }

            return null;


        }

        /**
        * Add a non Node related 3D object to the scene
        * @memberof FluidDiagrams
        * @function addSceneObject
        * @param {THREE.Mesh} geomObject
        */
        public addSceneObject(geomObject: any) {
            this.geometryObjects.push(geomObject);
            this.nodeSceneGraph.add(geomObject);
        }

         /**
        * Returns the node defined by it's unique id
        * @memberof FluidDiagrams
        * @function getNodeFromId
        * @param {int} id
        */
        public getNodeFromId(id) {
            for (var node in this.nodes) {
                if (this.nodes[node].getUniqueId() == id)
                    return this.nodes[node];
            }
        }
        public recalculateSize() {
            if (this.scale == true) {
                this.width = this.container.width();
                if (this.keepAspectRatio == true) {
                    this.height = this.container.width() * this.aspectRatio;
                } else {
                    this.height = this.container.height();
                }
                this.container.css("height", this.height);
                (<any>this.renderer).setSize(this.width, this.height);
            }
        }

        private onWindowResize(e: Event) {
            if (this.scale == true) {
                this.width = this.container.width();
                if (this.keepAspectRatio == true) {
                    this.height = this.container.width() * this.aspectRatio;
                } else {
                    this.height = this.container.height();
                }
                this.container.css("height", this.height);
                (<any>this.renderer).setSize(this.width, this.height);
            }
        }
    }
}
