/**
 * @author    Junho Jin[junho.jin@kaist.ac.kr] | https://github.com/JinJunho
 * @version   1.0.0
 *
 * [Project] Madeleine.js, Pure JavaScript STL Parser & Renderer.
 *
 * [Description] Madeleine.js constists of three part: DataReader, Madeleine and Lily.
 * DataReader is a helper function to read binary data, which is a customized version
 * of DataView. Lily is a helper Object that manages created Madeleines and logs
 * messages to console. Madeleine is the part that deals with actual parsing, rendering
 * and showing your stl files. Customize them however you want. This project is under
 * MIT License (see the LICENSE file for details). You are allowed to do anything with
 * this code, as long as you leave the attribution (MUST!). It will be glad if you
 * contact me for any bug you found or interesting ideas to do with Madeleine.js.
 * I'm willing to co-work with you!
 */

(function() {

  // Madeleine constructor
  Madeleine = function(options) {

    var Madeleine;

    // Constants for default setting
    var CONVERT_TO_BINARY = true;

    var OBJECT_MATERIAL   = "matt";
    var OBJECT_STATUS     = false;
    //var OBJECT_BACKGROUND = "DADADA";
    var OBJECT_BACKGROUND = "2E323E";
    var OBJECT_COLOR      = "00A0E1";

    var CAMERA_SIGHT     = 45;
    var CAMERA_NEARFIELD = 1;
    var CAMERA_FARFIELD  = 100000;

    var VIEWER_THEME  = "default";
    var VIEWER_PREFIX = "mad-";
    var VIEWER_CREATE = true;
    var VIEWER_HEIGHT = 625; //400
    var VIEWER_WIDTH  = 1000; //640

    var USER_ROTATE_SENSITIVITY = 0.005;
    var USER_ZOOM_SENSITIVITY = 100;
    var SCROLL_FACTOR = 10;

    // Necessary option check
    if (!document.getElementById(options.target)) {
      console.log("MADELEINE[ERR] Target must be a valid DOM Element.");
      return null;
    } else if (!options.data) {
      console.log("MADELEINE[ERR] Option must contain target and data.");
      return null;
    }

    // Construct new Madeleine
    Madeleine = function(options) {
      // Alias to own object
      var scope = this;
      // Internal properties
      this.__uniqueID     = Lily.push(this);
      this.__containerID  = options.target;
      this.__timer        = {start: (new Date).getTime(), end: null};
      // About 3d model
      this.__status       = null;
      this.__object       = null;
      this.__bounds       = null;
      this.__center       = null;
      this.__rawText      = null;
      this.__converted    = null;
      this.__arrayBuffer  = null;
      // Initialize data info object
      this.__info = {type: null, load: this.type, vertices: 0, facets: 0, faces: 0};
      // About visualization
      this.__scene    = new THREE.Scene();
      this.__camera   = null;
      this.__viewer   = null;
      this.__canvas   = null;
      // About rendering
      this.__geometry = null;
      this.__renderer = null;
      // About camera view
      this.__width     = null;
      this.__height    = null;
      this.__sizeRatio = 1;
      // About user interaction
      this.__firstPerson = false;
      this.__movable     = true;
      this.__zoomable    = true;
      this.__rotatable   = true;
      this.__rotating    = false;
      this.__trackMouse  = false;
      this.__mouseX      = 0;
      this.__mouseY      = 0;
      // Crucial properties to render 3d model
      this.data       = options.data;
      this.type       = options.type ? options.type : 'file';
      this.container  = document.getElementById(this.__containerID);
      this.relPath    = options.path ? options.path + (options.path[options.path.length-1] == "/" ? "" : "/") : "./";
      // User configuration
      this.options = Lily.extend(true, {}, { // Default option
        material        : OBJECT_MATERIAL,
        showStatus      : OBJECT_STATUS,
        backgroundColor : OBJECT_BACKGROUND,
        objectColor     : OBJECT_COLOR,
        viewer : {
          create        : VIEWER_CREATE,  // Create new viewer?
          prefix        : VIEWER_PREFIX,  // Viewer id prefix
          height        : VIEWER_HEIGHT,  // Viewer height
          width         : VIEWER_WIDTH,   // Viewer width
          theme         : VIEWER_THEME,   // Viewer theme
        },
        camera : {
          sight         : CAMERA_SIGHT,     // Vertical Field of View
          near          : CAMERA_NEARFIELD, // Near Field Distance
          far           : CAMERA_FARFIELD,  // Far Field Distance
        },
        rotateSensitivity : USER_ROTATE_SENSITIVITY,
        scrollFactor      : SCROLL_FACTOR,
        zoomSensitivity   : USER_ZOOM_SENSITIVITY,
      }, options);

      // Event Listeners
      this.scrollHandler = function(e) {
        var delta = e.wheelDelta ? e.wheelDelta/40 : (e.detail ? -e.detail : 0);
        if (delta < 0) delta -= this.options.scrollFactor;
        if (delta > 0) delta += this.options.scrollFactor;
        scope.cameraZoom(delta);
        e.preventDefault();
      }.bind(this);
      this.gestureHandler = function(e) {
        scope.cameraZoom(( 1 < e.scale ? "in" : "out" ));
        e.preventDefault();
      };
      this.mouseDownHandler = function(e) {
        e.preventDefault();
        if ((e.which && e.which == 3) || (e.button && e.button == 2)) {
          // alert("Right click on 3D viewer is disabled.");
        } else {
          scope.trackMouse = true;
          scope.__rotating = false;
          scope.mouseY = e.clientY;
          scope.mouseX = e.clientX;
        }
      };
      this.mouseMoveHandler = function(e) {
        if (scope.trackMouse) {
          // Top-left corner is (0, 0)
          // e.clientX grows as mouse goes down
          // e.clientY grows as mouse goes right

          //IMPROVED move object on wheel drag and rotate with left mouse drag
          if (e.which == 1) {//left button mouse
              scope.rotateObjectZ(scope.mouseX - e.clientX);
              scope.rotateObjectX(scope.mouseY - e.clientY);
          }

          if (e.which == 2) {//whell button mouse
              scope.moveObject(event, scope.mouseX - e.clientX);
              scope.moveObject(event, scope.mouseY - e.clientY);
          }
          scope.mouseY = e.clientY;
          scope.mouseX = e.clientX;
        }
        e.preventDefault();
      };
      this.mouseUpHandler = function(e) {
        scope.trackMouse = false;
        scope.__rotating = true;
        e.preventDefault();
      };
      this.touchStartHandler = function(e) {
        if (e.changedTouches.length == 1) {
          scope.trackMouse = true;
          scope.__rotating = false;
          scope.mouseY = e.changedTouches[0].clientY;
          scope.mouseX = e.changedTouches[0].clientX;
        }
      };
      this.touchMoveHandler = function(e) {
        if (scope.trackMouse) {
          // Top-left corner is (0, 0)
          // e.clientX grows as touch goes down
          // e.clientY grows as touch goes right
          scope.rotateObjectZ(scope.mouseX - e.clientX);
          scope.rotateObjectX(scope.mouseY - e.clientY);
          scope.mouseY = e.clientY;
          scope.mouseX = e.clientX;
        }
        e.preventDefault();
      };
      this.touchEndHandler = function(e) {
        scope.trackMouse = false;
        scope.__rotating = true;
        e.preventDefault();
      };
      this.rightClickHandler = function(e) {
        e.preventDefault();
      };
      this.viewModeHandler = function(e) {
        if (scope.__firstPerson) {
          e.target.className = e.target.className.replace(" focused", "");
          scope.__firstPerson = false;
          scope.disableFirstPersonViewerMode();
        } else {
          e.target.className += " focused";
          scope.__firstPerson = true;
          scope.enableFirstPersonViewerMode();
        }
      };
      this.captureHandler = function(e) {
        // TODO capture the model
      };

      // Check if option values are correct
      this.adjustUserConfiguration();
      // Initialize rendering
      this.init();
    };

    // Initialize rendering
    Madeleine.prototype.init = function() {

      //IMPROVE callbackstart process i.e.: show another loader in other scope
      if (this.options.callbackstart) this.options.callbackstart();

      // Get file name
      this.__info.name = (typeof this.data == "string") ? this.data.split("/").slice(-1)[0] : this.data.name;

      // If create new viewer, set canvas size to the viewer.
        if (this.options.viewer.create) {
          this.__height = this.options.viewer.height;
          this.__width  = this.options.viewer.width;
        // Get target width and height, otherwise.
      } else
      if (document.defaultView && document.defaultView.getComputedStyle) {
        this.__height = parseFloat(document.defaultView.getComputedStyle(this.container,null).getPropertyValue('height'));
        this.__width  = parseFloat(document.defaultView.getComputedStyle(this.container,null).getPropertyValue('width'));
      } else {
        this.__height = parseFloat(this.container.currentStyle.height);
        this.__width  = parseFloat(this.container.currentStyle.width);
      }
      // Create viewer
      this.createViewer();

      // Adjust canvas width/height ratio
      this.__sizeRatio = this.__width / this.__height;

      // Create camera
      this.__camera = new THREE.PerspectiveCamera(
        this.options.camera.sight,
        this.__sizeRatio,
        this.options.camera.near,
        this.options.camera.far
      );

      // Set user's point-of-view and default camera position
      this.__camera.position.set(0, 0, 0);
      this.__scene.add(this.__camera);

      // Add lights
      if (OBJECT_MATERIAL == "shiny") {
        this.addShadowedLight(1, 1, 1, 0xFFFFFF, 1.3);
        this.addShadowedLight(0.5, 1, -1, 0xFFCC66, 1);
        var ambientLight = new THREE.AmbientLight(0x111111);
        this.__scene.add(ambientLight);
      } else {
        var directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1);
        var ambientLight = new THREE.AmbientLight(0x202020);
        directionalLight.position.set(0, 1, 1).normalize();
        this.__scene.add(directionalLight);
        this.__scene.add(ambientLight);
      }

      // Start rendering
      this.draw();
    };

    // Rendering start
    Madeleine.prototype.draw = function() {
      // Wait until data is fully loaded
      var queued = (function(scope) {
        return function() {
          // When data ready, parse and render it.
          scope.run(scope.relPath + "lib/MadeleineLoader.js", {
            arrbuf: scope.__arrayBuffer,
            rawtext: scope.__rawText
          }, function(result) {
            var hasColors = result.hasColors;
            var vertices = result.vertices;
            var normals = result.normals;
            var colors = result.colors;
            var alpha = result.alpha;

            // Create new geometry
            scope.__geometry = new THREE.Geometry();

            // Parsing done. Add vertices and normals to geometry
            scope.__geometry = new THREE.BufferGeometry();
            scope.__geometry.addAttribute('position', new THREE.BufferAttribute(vertices, 3));
            scope.__geometry.addAttribute('normal', new THREE.BufferAttribute(normals, 3));

            // Set color information
            if (hasColors) {
              scope.__geometry.addAttribute('color', new THREE.BufferAttribute(colors, 3));
              scope.__geometry.hasColors = true;
              scope.__geometry.alpha = alpha;
            }

            // Start rendering
            scope.renderObject();

            // Compute time consumed in parsing and rendering
            scope.__timer.end = (new Date()).getTime();
            var consumed = (scope.__timer.end - scope.__timer.start) / 1000;
            console.log("MADELEINE[LOG] Time spent: " + consumed + " sec.");

            // Render object
            scope.render();
            // Log rendering status
            scope.logStatus();
            // Start rotating animation
            scope.startAnimation();
            // Enable mouse zoom action
            scope.enableZoomAsMouseScroll();
            // Enable mouse motion action
            scope.enableUserInteraction();

            //IMPROVED callback end i.e: hide loading of another scope
            if (scope.options.callbackend) scope.options.callbackend();

          });
        };
      })(this);

      // Check input type and get STL Binary data
      switch (this.type) {
        case "upload":
          this.getDataFromBlob(this.data, queued);break;
        case "file":
          this.getDataFromUrl(this.data, queued);break;
        default:
          break;
      }
    };

    // Get arrayBuffer from Blob
    Madeleine.prototype.getDataFromBlob = function(file, queuedWork) {
      var scope = this;

      if (Detector.fileapi) {
          var arrbuf = new FileReader();
          var rawtxt = new FileReader();
          // arraybuffer onload function
          arrbuf.onload = function () {
              scope.__arrayBuffer = arrbuf.result
              //console.log("scope.__arrayBuffer------>", scope.__arrayBuffer);
              if (scope.__arrayBuffer && scope.__rawText) {//IMPROVED asynchronous onload arrbuf.onload rawtxt.onload doesnt know witch will finish first causing null __arrayBuffer or null __rawText
                  console.log('calling o queuedWork no arrbuf onload');
                  queuedWork();
              }
          };
          // read arrayBuffer from Blob
          arrbuf.readAsArrayBuffer(file);

          // rawtext onload function
          rawtxt.onload = function () {
              scope.__rawText = rawtxt.result;
              //console.log("scope.__rawText ----->", scope.__rawText);
              if (scope.__arrayBuffer && scope.__rawText) {//IMPROVED asynchronous onload arrbuf.onload rawtxt.onload doesnt know witch will finish first causing null __arrayBuffer or null __rawText
                  console.log('calling in no rawtxt onload');
                  queuedWork();
              }
          };
          // read raw text data from Blob
          rawtxt.readAsText(file);
      }
    };

    // Get arrayBuffer from external file
    Madeleine.prototype.getDataFromUrl = function(url, queuedWork, type) {
      var scope = this;
      var getArrayBuffer = function() {
        var arrbuf = new XMLHttpRequest();
        arrbuf.onerror = function(e) { console.log("MADELEINE[ERR] Ajax failed.") };
        arrbuf.onreadystatechange = function() {
          if (arrbuf.readyState == 4 && (arrbuf.status == 200 || arrbuf.status == 0)) {
            scope.__arrayBuffer = arrbuf.response;
            if (type == "binary") queuedWork();
            if (!type) getRawText();
          }
        };
        arrbuf.responseType = "arraybuffer";
        arrbuf.open("GET", url, true);
        arrbuf.send(null);
      };
      var getRawText = function() {
        var rawtxt = new XMLHttpRequest();
        rawtxt.onerror = function(e) { console.log("MADELEINE[ERR] Ajax failed.") };
        rawtxt.onreadystatechange = function() {
          if (rawtxt.readyState == 4 && (rawtxt.status == 200 || rawtxt.status == 0)) {
            scope.__rawText = rawtxt.response;
            queuedWork();
          }
        };
        rawtxt.responseType = "text";
        rawtxt.open("GET", url, true);
        rawtxt.send(null);
      };

      // Get data from external resource
      switch(type) {
        case "ascii":
          getRawText();
          break;
        default:
          getArrayBuffer();
          break;
      }
    };

    // Render object
    Madeleine.prototype.renderObject = function() {
      // Create renderer
      if (!this.__object) this.createRenderer();

      // Get material
      var material = null;
      if (this.options.material == "shiny") {
        material = new THREE.MeshPhongMaterial({
          color: this.getHexColor(this.options.objectColor),
          ambient: this.getHexColor(this.options.objectColor),
          specular: 0x1F1F1F,
          shininess: 25,
          side: THREE.DoubleSide,
          overdraw: true
        });
      } else {
        material = new THREE.MeshLambertMaterial({
          color: this.getHexColor(this.options.objectColor),
          shading: THREE.FlatShading,
          side: THREE.DoubleSide,
          overdraw: true
        });
      }

      // Generate mesh for object
      this.__object = new THREE.Mesh(this.__geometry, material);
      this.__geometry.computeBoundingSphere();
      this.__geometry.computeVertexNormals();
      this.__geometry.computeBoundingBox();

      // Adjust positions of camera and object to make object fit into screen properly
      var radius = this.__geometry.boundingSphere.radius;
      // 1.865 times zoomed object with radius 85 practically fits best to the screen
      var zoomFactor = 1.865 * 85 / radius;

      // Zoom object to fit into the screen. ZoomFactor is practically best to fit
      this.__object.scale.set(zoomFactor, zoomFactor, zoomFactor);
      // Height 125 fits well to the screen. If taller, lower the center of y-axis
      var maxY = this.__geometry.boundingBox.max.y;
      var minY = this.__geometry.boundingBox.min.y;
      var height = maxY - minY;
      var deltaY = (height > 125 ? parseFloat(0.99 * (height - Math.max(maxY, Math.abs(minY)))) : 15);
      // deltaY in some cases can make object out of screen height
      //console.log(deltaY, height, minY, maxY);
      if (options.XY) {//IMPROVMENT setXY mannually
          if (options.XY.X) {
              this.__object.position.setX(options.XY.X);
          }
          if (options.XY.Y) {
              this.__object.position.setY(options.XY.Y);
          }
      } else {
          this.__object.position.setY(-deltaY);
      }

      // If object is too large to fit in, make camera look further
      // 500 (default camera distance) : 466 (view height)
      // new distance (x) : boundingSphere radius (r) * 2
      // x = (2 * r * 500) / 466 ~= 2.146 * r
      if (radius < 233) this.__camera.position.z = 500;
      else this.__camera.position.z = radius * 2.146;
      this.__camera.updateProjectionMatrix();

      // Parsing finished
      this.__scene.add(this.__object);
      this.__object.rotation.x = -1.2;
      this.__object.rotation.z = 1.2;
    };

    // Generate Renderer
    Madeleine.prototype.createRenderer = function() {
      // create renderer
      this.__renderer = Detector.webgl ?
        new THREE.WebGLRenderer({
          preserveDrawingBuffer: true,
          alpha: true
        }) : new THREE.CanvasRenderer();
      // attach canvas to viewer
      this.__canvas = this.__renderer.domElement;
      this.__viewer.appendChild(this.__canvas);
      // renderer configuration
      this.__renderer.setSize(this.__width, this.__height);
      this.__renderer.setClearColor(0x000000, 0);
      // this.__renderer.shadowMapCullFace = THREE.CullFaceBack;
      // this.__renderer.shadowMapEnabled = true;
      // this.__renderer.gammaOutput = true;
      // this.__renderer.gammaInput = true;
    };

    // Generate Madeleine Viewer
    Madeleine.prototype.createViewer = function() {
      if (!this.options.viewer.create) this.container.appendChild(this.__renderer.domElement);
      else {
        // Create viewer element
        this.__viewer = document.createElement("div");
        this.__viewer.id = this.options.viewer.prefix + this.__uniqueID;

        // Force viewer size
        //this.container.style["max-height"] = this.__height+"px";
        //this.container.style["min-height"] = this.__height+"px";
        //this.__viewer.style["max-height"] = this.__height+"px";
        //this.__viewer.style["min-height"] = this.__height+"px";
        //this.__viewer.style["max-width"] = this.__width+"px";
        this.__viewer.style["max-width"] = "100%";
        //this.__viewer.style["min-width"] = this.__width+"px";
        this.__viewer.style["min-width"] = "50%";
        this.__viewer.style.height = this.__height;
        this.__viewer.style.width = this.__width;

        // Set default style
        this.__viewer.style.background = "transparent";
        this.__viewer.style.position = "relative";
        this.__viewer.style.margin = "0 0 10px 0";

        // Progress bar
        var progress = document.createElement("div");
        progress.id = "progressBar-" + this.__uniqueID;
        progress.style["-webkit-transition"] = "width .5s ease-in-out, opacity 2s ease";
        progress.style["-moz-transition"] = "width .5s ease-in-out, opacity 2s ease";
        progress.style["-ms-transition"] = "width .5s ease-in-out, opacity 2s ease";
        progress.style["-o-transition"] = "width .5s ease-in-out, opacity 2s ease";
        progress.style["transition"] = "width .5s ease-in-out, opacity 2s ease";
        progress.style["min-height"] = "2px";
        progress.style.background = "#009999";
        progress.style.position = "absolute";
        progress.style.height = "2px";
        progress.style.width = 0;
        progress.style.left = 0;
        progress.style.top = 0;

        // Viewer iconGrid
        var iconGrid = document.createElement("div");
        iconGrid.style.cssText += "background:transparent;position:absolute;padding:15px 10px;";
        //iconGrid.style.cssText += "height:50px;width:"+this.__width+"px;top:0;overflow:hidden;";
        iconGrid.style.cssText += "height:50px;width:100%;top:0;overflow:hidden;";
        iconGrid.className += "box";

        var logo = document.createElement("div");
        var info = document.createElement("div");
        var view = document.createElement("div");
        var capture = document.createElement("div");
        //var download = document.createElement("div");
        var fullscreen = document.createElement("div");

        info.id = "model-info-" + this.__uniqueID;
        info.className += "model-info noselect";
        info.innerHTML = this.__info.name;

        logo.className += "clickable pull-left madeleine-logo";
        view.className += "clickable pull-right icon-mad-view";
        capture.className += "clickable pull-right icon-mad-capture";
        //download.className += "clickable pull-right icon-mad-download";
        fullscreen.className += "clickable pull-right icon-mad-screen-full";

        Lily.bind(view, "click", this.viewModeHandler);
        Lily.bind(capture, "click", this.captureHandler);

        var rotator = document.createElement("div");
        var faster = document.createElement("div");
        var slower = document.createElement("div");
        var player = document.createElement("div");

        rotator.style.cssText += "background:transparent;position:absolute;padding:15px 10px;right:0;";
        //rotator.style.cssText += "height:50px;width:"+this.__width+"px;top:0;overflow:hidden;";
        rotator.style.cssText += "height:50px;width:100%;top:0;overflow:hidden;";
        rotator.style.cssText += "margin-top:"+(this.__height-30)+"px;";

        player.className += "icon-clickable pull-right icon-mad-stop";
        slower.className += "icon-clickable pull-right icon-mad-slower";
        faster.className += "icon-clickable pull-right icon-mad-faster";

        rotator.appendChild(faster);
        rotator.appendChild(player);
        rotator.appendChild(slower);

        var controller = document.createElement("div");
        var trackball = document.createElement("div");
        var right = document.createElement("div");
        var left = document.createElement("div");
        var down = document.createElement("div");
        var up = document.createElement("div");

        iconGrid.appendChild(fullscreen);
        //iconGrid.appendChild(download);
        iconGrid.appendChild(capture);
        iconGrid.appendChild(view);
        iconGrid.appendChild(logo);
        iconGrid.appendChild(info);
        iconGrid.appendChild(rotator);

        // Append to container
        if (this.options.viewer.notappend) {//IMPROVED not append a new canvas put in same
            $(this.container).html(this.__viewer);
            this.__viewer.appendChild(iconGrid);
            this.__viewer.appendChild(progress);
        } else {
            this.container.appendChild(this.__viewer);
            this.__viewer.appendChild(iconGrid);
            this.__viewer.appendChild(progress);
        }
        this.adaptViewerTheme();
      }
    };

    // Set viewer theme
    Madeleine.prototype.adaptViewerTheme = function() {
      var theme = arguments.length == 0 ? this.options.viewer.theme : arguments[0];

      // Adapt theme
      switch (theme) {
        case "dark":
          this.__viewer.style.background  = "#000000";
          this.options.objectColor = "FFD300";
          break;
        case "lime":
          this.__viewer.style.cssText += this.generateGradation({dark: "2B2B2B"});
          this.options.objectColor = "D4FF00"; // [212, 255, 0];
          break;
        case "rose":
          this.__viewer.style.cssText += this.generateGradation({bright: "369075"});
          this.options.objectColor = "C94C66"; // [201, 76, 102];
          break;
        case "lego":
          this.__viewer.style.cssText += this.generateGradation({bright: "FFA400"});
          this.options.objectColor = "00A08C"; // [0, 160, 140];
          break;
        case "toxic":
          this.__viewer.style.cssText += this.generateGradation({bright: "FFEE4D"});
          this.options.objectColor = "5254CB"; // [82, 84, 203];
          break;
        case "cobalt":
          this.__viewer.style.cssText += this.generateGradation({bright: "FFC200"});
          this.options.objectColor = "0C6BC0"; // [12, 107, 192];
          break;
        case "light":
          this.__viewer.style.cssText += this.generateGradation({bright: "FFFFFF"});
          this.options.objectColor = "F00842";
          break;
        case "soft":
          this.__viewer.style.cssText += this.generateGradation({dark: "0F0F0F", bright: "4D4D4D", pos1: "0", pos2: "60"});
          this.options.objectColor = OBJECT_COLOR;
          break;
        default:
          this.__viewer.style.background  = this.makeHexString(options.backgroundColor || OBJECT_BACKGROUND);
          this.options.objectColor = options.objectColor || OBJECT_COLOR;
          break;
      }

      // If object exists, paint color on it.
      this.__object && this.setObjectColor();
    };

    // Set canvas background color
    Madeleine.prototype.setBackgroundColor = function(code) {
      var code = arguments.length == 3 ? [arguments[0], arguments[1], arguments[2]] : code;
      var color = this.getHexString(code);
      this.__viewer.style.background = this.makeHexString(color);
    };

    // Set object surface color
    Madeleine.prototype.setObjectColor = function() {
      var color = arguments.length != 0 ? arguments : this.options.objectColor;
      this.__object.material.color.setHex(this.getHexColor(color));
    };

    // Generate gradation css
    Madeleine.prototype.generateGradation = function(colors) {
      var pos1, pos2, darker, brighter, cssText;

      cssText = "background: BRIGHT;" +
                "background: -moz-radial-gradient(center, ellipse cover, BRIGHT POS1%, DARK POS2%);" +
                "background: -webkit-gradient(radial, center center, 0px, center center, POS2%, color-stop(POS1%,BRIGHT), color-stop(POS2%,DARK))" +
                "background: -webkit-radial-gradient(center, ellipse cover, BRIGHT POS1%,DARK POS2%);" +
                "background: -o-radial-gradient(center, ellipse cover, BRIGHT POS1%, DARK POS2%);" +
                "background: -ms-radial-gradient(center, ellipse cover, BRIGHT POS1%, DARK POS2%);" +
                "background: radial-gradient(ellipse at center, BRIGHT POS1%, DARK POS2%);" +
                "filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='BRIGHT', endColorstr='DARK',GradientType=1)";

      if (colors.dark) {
        darker = this.getHexString(colors.dark);
        brighter = this.getHexString(colors.bright ? colors.bright : this.adjustBrightness(darker));
      } else {
        brighter = this.getHexString(colors.bright);
        darker = this.getHexString(this.adjustBrightness(brighter));
      }

      pos1 = colors.pos1 ? colors.pos1 : 27;
      pos2 = colors.pos2 ? colors.pos2 : 100;

      return cssText.replace(/BRIGHT/g, brighter).replace(/DARK/g, darker).replace(/POS1/g, pos1).replace(/POS2/g, pos2);
    };

    // Get Hex color (0xXXXXXX format)
    Madeleine.prototype.getHexColor = function() {
      var code = arguments.length == 3 ? [arguments[0], arguments[1], arguments[2]] : arguments[0];
      var color = this.getHexString(code);
      return parseInt(color.replace(/\#/g, ''), 16);
    };

    // Get Hex-style string (#XXXXXX format)
    Madeleine.prototype.getHexString = function(code) {
      if (typeof code == "string") return this.makeHexString(code);
      else if (typeof code == "object") return this.rgbToHex(code);
    };

    // Strip # from Hex-style string
    Madeleine.prototype.cutHexString = function(code) {
      return code.charAt(0) == "#" ? code.substring(1) : code;
    };

    // Prepend # to hex string
    Madeleine.prototype.makeHexString = function(code) {
      var color = code.length == 3 ? code.replace(/(.)/g, '$1$1') : code;
      return color.charAt(0) != "#" ? "#" + color : color;
    };

    // Convert RGB to Hex
    Madeleine.prototype.rgbToHex = function(code) {
      var i, color, hex = "#";
      for (i = 0; i < code.length; i++) {
        color = code[i].toString(16);
        hex += (color.length == 1 ? "0" + color : color);
      }
      return hex;
    };

    // Convert Hex to RGB
    Madeleine.prototype.hexToRgb = function(code) {
      var dec = this.cutHexString(code).toString(16);
      return [parseInt(dec.substring(0, 2), 16),
              parseInt(dec.substring(2, 4), 16),
              parseInt(dec.substring(4, 6), 16)];
    };

    // Get brighter or darker color from input color
    // Brightness is automatically set according to its intensity.
    Madeleine.prototype.adjustBrightness = function(code) {
      var adjustedR, adjustedG, adjustedB;
      var intensity, color, rgb, r, g, b;
      var darkFactor = 30, brightFactor = 20;

      // Get color and RGB
      color = this.cutHexString(code);
      rgb = this.hexToRgb(color);

      r = rgb[0];
      g = rgb[1];
      b = rgb[2];

      // Get grayscale intensity
      intensity = Math.sqrt(r*r + g*g + b*b) / 255 * 100;

      // New color
      adjustedR = intensity > 40 ? (0|(1<<8) + r * (1 - darkFactor / 100)) : (0|(1<<8) + r + (256 - r) * brightFactor / 100);
      adjustedG = intensity > 40 ? (0|(1<<8) + g * (1 - darkFactor / 100)) : (0|(1<<8) + g + (256 - g) * brightFactor / 100);
      adjustedB = intensity > 40 ? (0|(1<<8) + b * (1 - darkFactor / 100)) : (0|(1<<8) + b + (256 - b) * brightFactor / 100);

      return (adjustedR.toString(16)).substr(1) +
             (adjustedG.toString(16)).substr(1) +
             (adjustedB.toString(16)).substr(1);
    };

    // Add new background worker
    Madeleine.prototype.run = function(path, luggage, onload) {
      var progressBar = document.getElementById("progressBar-"+this.__uniqueID);
      var worker = new WorkerFacade(path);
      var scope = this;

      worker.postMessage(luggage);
      worker.onmessage = function(event) {
        var result = event.data;
        switch (result.type) {
          case "convert-progress":
            // console.log("MADELEINE[LOG] Background converting progress: " + result.data + "%");
            break;
          case "convert":
            if (CONVERT_TO_BINARY) scope.run(scope.relPath + "lib/MadeleineConverter.js", result.data, function(result) { scope.__converted = result });
            break;
          case "progress":
            //progressBar.style.width = (scope.__width * result.data / 100) + "px";
            progressBar.style.width = (result.data) + "%";
            if (result.data == 100) {
              progressBar.style["-webkit-opacity"] = 0;
              progressBar.style["-moz-opacity"] = 0;
              progressBar.style["opacity"] = 0;
            };
            // console.log("MADELEINE[LOG] Background work progress: " + result.data + "%");
            break;
          case "message":
            console.log("MADELEINE[LOG] Background work message: " + result.data);
            break;
          case "error":
            console.log("MADELEINE[ERR] Background work error: " + result.data);
            break;
          case "info":
            scope.__info[result.prop] = result.data;
            if (result.prop == "size") {
              var infoBox = document.getElementById("model-info-" + scope.__uniqueID);
              infoBox.innerHTML += " (" +result.data + ")";
            }
            break;
          case "data":
            scope["__" + result.prop] = result.data;
            break;
          case "done":
            onload(result.data);
            break;
          default:
            // Do nothing
            break;
        }
      };
    };

    // Start animation
    Madeleine.prototype.startAnimation = function() {
      if (this.__rotatable) this.__rotating = true;
      this.interact(this);
    };

    // Stop animation
    Madeleine.prototype.stopAnimation = function() {
      this.__rotating = false;
    };

    // Start rotating object
    Madeleine.prototype.startRotation = function() {
      this.options.rotateSensitivity = 0.005;
      this.__rotating = true;
    };

    // Stop rotating object
    Madeleine.prototype.stopRotation = function() {
      this.__rotating = false;
    };

    // Enable user interaction
    Madeleine.prototype.interact = function(scope) {
      requestAnimationFrame(function(){scope.interact(scope)});
      scope.options.showStatus && scope.__status.update();
      scope.render();
    };

    // Perform actual rendering object
    Madeleine.prototype.render = function() {
      this.__rotating && this.rotateObjectZ(-1);
      this.__renderer.render(this.__scene, this.__camera);
    };

    // Move object in xy //IMPROVED
    let oldX = 0, oldY = 0;
    Madeleine.prototype.moveObject = function ($event, delta) {
        if (this.__movable) {
            let directionX = 0, directionY = 0, diffX = 0, diffY = 0;
            let offsetmove = options.MOVE_FACTOR || 3;

            if ($event.pageX < oldX) {
                directionX = "left"
                diffX = oldX - $event.pageX;
            } else if ($event.pageX > oldX) {
                directionX = "right"
                diffX = $event.pageX - oldX;
            }

            if ($event.pageY < oldY) {
                directionY = "top"
                diffY = oldY - $event.pageY;
            } else if ($event.pageY > oldY) {
                directionY = "bottom";
                diffY = $event.pageY - oldY;
            }

            oldX = $event.pageX;
            oldY = $event.pageY;

            if (directionX == "left") this.__object.position.x -= offsetmove;
            if (directionX == "right") this.__object.position.x += offsetmove;

            if (directionY == "bottom") this.__object.position.y -= offsetmove;
            if (directionY == "top") this.__object.position.y += offsetmove;
        }
    };

    // Rotate object in X direction
    Madeleine.prototype.rotateObjectX = function(delta) {
      if (this.__movable) this.__object.rotation.x -= delta * this.options.rotateSensitivity;
    };

    // Rotate object in Y direction
    Madeleine.prototype.rotateObjectY = function(delta) {
      if (this.__movable) this.__object.rotation.y -= delta * this.options.rotateSensitivity;
    };

    // Rotate object in Z direction
    Madeleine.prototype.rotateObjectZ = function(delta) {
      if (this.__movable) this.__object.rotation.z -= delta * this.options.rotateSensitivity;
    };

    // Make animation faster as much as 'delta'
    Madeleine.prototype.animationFaster = function(delta) {
      this.options.rotateSensitivity *= (delta ? delta : 2);
    };

    // Make animation slower as much as 'delta'
    Madeleine.prototype.animationSlower = function(delta) {
      this.options.rotateSensitivity /= (delta ? delta : 2);
    };

    // Disable Madeline Viewer to be zoomed by mouse scroll
    Madeleine.prototype.disableZoomAsMouseScroll = function() {
      var target = this.container;
      // remove event handler
      Lily.remove(this.container, "mousewheel", this.scrollHandler);
      Lily.remove(this.container, "DOMMouseScroll", this.scrollHandler);
      Lily.remove(this.container, "gesturechange", this.gestureHandler);
    };

    // Enable Madeline Viewer to be zoomed by mouse scroll
    Madeleine.prototype.enableZoomAsMouseScroll = function() {
      if (!this.__zoomable) return;
      // attach event handler
      Lily.bind(this.container, "mousewheel", this.scrollHandler);
      Lily.bind(this.container, "DOMMouseScroll", this.scrollHandler);
      Lily.bind(this.container, "gesturechange", this.gestureHandler);
    };

    // Enable Madeline Viewer to be controlled by mouse movement
    Madeleine.prototype.enableUserInteraction = function() {
      // block right click action
      Lily.bind(this.container, "contextmenu", this.rightClickHandler);
      if (!this.__movable) return;
      // attach event handler
      Lily.bind(this.container, "mousedown", this.mouseDownHandler);
      Lily.bind(this.container, "mousemove", this.mouseMoveHandler);
      Lily.bind(this.container, "mouseup", this.mouseUpHandler);
      // mobile support
      Lily.bind(this.container, "touchstart", this.touchStartHandler);
      Lily.bind(this.container, "touchmove", this.touchMoveHandler);
      Lily.bind(this.container, "touchend", this.touchEndHandler);
    };

    // Disable Madeline Viewer to be controlled by mouse movement
    Madeleine.prototype.disableUserInteraction = function() {
      if (!this.__movable) return;
      // attach event handler
      Lily.remove(this.container, "mousedown", this.mouseDownHandler);
      Lily.remove(this.container, "mousemove", this.mouseMoveHandler);
      Lily.remove(this.container, "mouseup", this.mouseUpHandler);
      // mobile support
      Lily.remove(this.container, "touchstart", this.touchStartHandler);
      Lily.remove(this.container, "touchmove", this.touchMoveHandler);
      Lily.remove(this.container, "touchend", this.touchEndHandler);
    };

    // Enable first-person viewer mode
    Madeleine.prototype.enableFirstPersonViewerMode = function() {
      this.disableUserInteraction();
      Lily.bind(document, "keypress", this.firstPersonHandler);
      Lily.bind(document, "keydown", this.firstPersonHandler);
      this.__firstPerson = true;
      this.__rotating = false;
      this.__zoomable = false;
      this.__movable = false;
    };

    // Disable first-person viewer mode
    Madeleine.prototype.disableFirstPersonViewerMode = function() {
      this.enableUserInteraction();
      this.__firstPerson = false;
      this.__rotating = true;
      this.__zoomable = true;
      this.__movable = true;
    };

    // Adjust camera zoom in/out
    Madeleine.prototype.cameraZoom = function() {
      if (!this.__zoomable) return;
      else {
        var delta, type;
        if (arguments.length == 1) {
          if (typeof arguments[0] == "string") {
            delta = this.options.zoomSensitivity;
            type = arguments[0];
          } else {
            delta = arguments[0];
            type = null;
          }
        } else if (arguments.length == 2) {
          delta = arguments[0];
          type = arguments[1];
        }
        if (type == "in") this.__camera.position.z += delta;
        else this.__camera.position.z -= delta;
        this.__camera.updateProjectionMatrix();
      }
    };

    // Show animation status
    Madeleine.prototype.logStatus = function(target) {
      if (!this.options.showStatus) return;
      this.__status = new Stats();
      this.__status.domElement.style.position = 'absolute';
      this.__status.domElement.style.top = '0px';
      this.__viewer.appendChild(this.__status.domElement);
    };

    // Show animation status
    Madeleine.prototype.stopLogStatus = function(target) {
      this.options.showStatus = false;
      this.__status = null;
    };

    // Adjust material, camera settings and sensitivities to have proper values
    Madeleine.prototype.adjustUserConfiguration = function() {
      this.adjustRotateSensitivity();
      this.adjustZoomSensitivity();
      this.adjustFocalPoint();
      this.checkMaterial();
    };

    // Check material
    Madeleine.prototype.checkMaterial = function() {
      if (this.options.material !== "skin" || this.options.material !== "wire") {
        this.options.material !== "skin";
      }
    };

    // Adjust camera settings to fit into proper range
    Madeleine.prototype.adjustFocalPoint = function() {
      var sight = this.options.camera.sight;
      var near = this.options.camera.near;
      var far = this.options.camera.far;

      if (sight && typeof sight === "number" && 75 <= sight && sight <= 1000) return;
      else this.options.camera.sight = CAMERA_SIGHT;
      if (near && typeof near === "number" && 0 <= near && near <= 1000) return;
      else this.options.camera.near = CAMERA_NEARFIELD;
      if (far && typeof far === "number" && 5000 <= far && far <= 100000) return;
      else this.options.camera.far = CAMERA_FARFIELD;
    };

    // Adjust zoom sensitivity to fit into proper range
    Madeleine.prototype.adjustZoomSensitivity = function() {
      var intensity = this.options.zoomSensitivity;
      var visibleField = this.options.camera.far - this.options.camera.near;

      if (intensity && typeof intensity === "number" && 0 < intensity && intensity <= visibleField/1000) return;
      else this.options.zoomSensitivity = visibleField/1000;
    };

    // Adjust rotate sensitivity to fit into proper range
    Madeleine.prototype.adjustRotateSensitivity = function(intensity) {
      var intensity = this.options.rotateSensitivity;

      if (intensity && typeof intensity === "number" && 0 < intensity && intensity < 0.05) return;
      else this.options.rotateSensitivity = USER_ROTATE_SENSITIVITY;
    };

    // Add directional light
    Madeleine.prototype.addShadowedLight = function(x, y, z, color, intensity) {
      var directionalLight = new THREE.DirectionalLight(color, intensity);
      var d = 1;

      directionalLight.position.set(x, y, z);
      this.__scene.add(directionalLight);

      // directionalLight.shadowCameraVisible = true;
      directionalLight.castShadow = true;
      directionalLight.shadowCameraTop = d;
      directionalLight.shadowCameraLeft = -d;
      directionalLight.shadowCameraRight = d;
      directionalLight.shadowCameraBottom = -d;

      directionalLight.shadowCameraNear = 1;
      directionalLight.shadowCameraFar = 4;

      directionalLight.shadowMapWidth = 1024;
      directionalLight.shadowMapHeight = 1024;

      directionalLight.shadowBias = -0.005;
      directionalLight.shadowDarkness = 0.15;
    };

    //Set position
    Madeleine.prototype.setXY = function (X, Y) {//IMPROVED
        this.__object.position.setY(Y);
        this.__object.position.setX(X);

    }

    return new Madeleine(options);

  };

  // Lily helps madeleine.
  window.Lily = (function() {

    // Initialize
    var Lily = function() {
      if (!Detector.webgl) Detector.addGetWebGLMessage();
      this.sisters = [];
    };

    // Attach Madeleine to file input
    Lily.prototype.ready = function(options) {
      // Check option fields
      if (!options.file || !options.target) {
        console.log("MADELEINE[ERR] Option must contain target and file.");
        return null;
      }

      var target = document.getElementById(options.file);
      if (!target) {
        console.log("MADELEINE[ERR] Please provide valid input file element.");
        return null;
      }
      // Attach file upload event handler
      if (target.tagName.toLowerCase() == "input" && target.type.toLowerCase() == "file") this.onFileInputChange(target, options);

      // (Optional) Attach drag-and-drop event handler
      if (options.dropzone) {
        var dragOverHandler = function(e) {
          e.stopPropagation();
          e.preventDefault();
          e.dataTransfer.dropEffect = "copy";
        };
        var fileDropHandler = function(e) {
          var i, files = e.dataTransfer.files;
          e.stopPropagation();
          e.preventDefault();
          for (i = 0; i < files.length; i++) {
            // create Madeleine for each file
            var _options = window.Lily.extend({}, options, {type: "upload", data: files[i]});
            var madeleine = new Madeleine(_options);
          }
        };

        // Attach event handler
        target = document.getElementById(options.dropzone);
        this.bind(target, "dragover", dragOverHandler);
        this.bind(target, "drop", fileDropHandler);
      }
    };

    // Remove attached event handler from element
    Lily.prototype.remove = function(elem, event, doThis) {
      var attach = (elem.removeEventListener ? "removeEventListener" : (elem.detachEvent ? "detachEvent" : ""));
      var onEvent = (elem.removeEventListener ? event : "on" + event);

      if (attach == "") elem[onEvent] = doThis;
      else elem[attach](onEvent, doThis, false);
    };

    // Attach new event handler to element
    Lily.prototype.bind = function(elem, event, doThis) {
      var attach = (elem.addEventListener ? "addEventListener" : (elem.attachEvent ? "attachEvent" : ""));
      var onEvent = (elem.addEventListener ? event : "on" + event);

      if (attach == "") elem[onEvent] = doThis;
      else elem[attach](onEvent, doThis, false);
    };

    // jQuery source of extend function
    // Reference: https://github.com/jquery/jquery/blob/master/src/core.js
    Lily.prototype.extend = function() {
      var options, name, src, copy, copyIsArray, clone,
        target = arguments[0] || {},
        i = 1,
        length = arguments.length,
        deep = false,
        toString = Object.prototype.toString,
        hasOwn = Object.prototype.hasOwnProperty,
        push = Array.prototype.push,
        slice = Array.prototype.slice,
        trim = String.prototype.trim,
        indexOf = Array.prototype.indexOf,
        class2type = {"[object Boolean]": "boolean","[object Number]": "number","[object String]": "string","[object Function]": "function",
                      "[object Array]": "array","[object Date]": "date","[object RegExp]": "regexp","[object Object]": "object"},
        helper = {
          isFunction: function (obj) {return helper.type(obj) === "function"},
          isArray: Array.isArray || function (obj) {return helper.type(obj) === "array"},
          isWindow: function (obj) {return obj != null && obj == obj.window},
          isNumeric: function (obj) {return !isNaN(parseFloat(obj)) && isFinite(obj)},
          type: function (obj) {return obj == null ? String(obj) : class2type[toString.call(obj)] || "object"},
          isPlainObject: function (obj) {if (!obj || helper.type(obj) !== "object" || obj.nodeType) {return false}
            try { if (obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {return false}} catch (e) {return false}
            var key; for (key in obj) {}; return key === undefined || hasOwn.call(obj, key)
          }
        };
      // Handle a deep copy situation
      if ( typeof target === "boolean" ) {
        deep = target;
        // Skip the boolean and the target
        target = arguments[ i ] || {};
        i++;
      }
      // Handle case when target is a string or something (possible in deep copy)
      if ( typeof target !== "object" && !helper.isFunction(target) ) {
        target = {};
      }
      // Extend helper itself if only one argument is passed
      if ( i === length ) {
        target = this;
        i--;
      }
      for ( ; i < length; i++ ) {
        // Only deal with non-null/undefined values
        if ( (options = arguments[ i ]) != null ) {
          // Extend the base object
          for ( name in options ) {
            src = target[ name ];
            copy = options[ name ];
            // Prevent never-ending loop
            if ( target === copy ) { continue; }
            // Recurse if we're merging plain objects or arrays
            if ( deep && copy && ( helper.isPlainObject(copy) ||
              (copyIsArray = helper.isArray(copy)) ) ) {
              if ( copyIsArray ) {
                copyIsArray = false;
                clone = src && helper.isArray(src) ? src : [];
              } else { clone = src && helper.isPlainObject(src) ? src : {}; }
              // Never move original objects, clone them
              target[ name ] = this.extend( deep, clone, copy );
            // Don't bring in undefined values
            } else if ( copy !== undefined ) {
              target[ name ] = copy;
            }
          }
        }
      }
      // Return the modified object
      return target;
    };

    // When user uploads files, call Madeleine immediately for each stl file.
    Lily.prototype.onFileInputChange = function(input, options) {
      // event handler
      var fileInputChangeHandler = function() {
        var i, files = this.files;
        if (files.length) {
          for (i = 0; i < files.length; i++) {
            //IMPROVED - Only accepts stl files and invoke callbackerror and throws new error
            if (files[i].type != 'stl' && files[i].name.indexOf("stl") == -1) {
                if (options.callbackerror) options.callbackerror({ code: 1001, msg: "Invalid File" });
                throw new Error("Invalid Format File. Only Accepts STL!");
            }
            // create Madeleine for each file
            var _options = window.Lily.extend({}, options, {type: "upload", data: files[i]});
            var madeleine = new Madeleine(_options);
            // hide file input element
            input.style.display = "none";
          }
        }
      };

      // attach event handler
      this.bind(input, "change", fileInputChangeHandler);
    };

    // Put madeleine to madeleine list
    Lily.prototype.push = function(madeleine) {
      this.sisters.push(madeleine);
      return this.sisters.length-1;
    };

    // Get madeleine of id 'index'
    Lily.prototype.get = function(index) {
      return this.sisters[index];
    };

    return new Lily();

  })();

})();

/* A facade for the Web Worker API that fakes it in case it's missing.
 * Good when web workers aren't supported in the browser, but it's still fast enough,
 * so execution doesn't hang too badly (e.g. Opera 10.5).
 * By Stefan Wehrmeyer, licensed under MIT
 */
WorkerFacade = null;
if(!!window.Worker){
  WorkerFacade = (function(){
    return function(path){
      return new window.Worker(path);
    };
  }());
} else {
  WorkerFacade = (function(){
    var workers = {}, masters = {}, loaded = false;
    var that = function(path){
      var theworker = {}, loaded = false, callings = [];
      theworker.postToWorkerFunction = function(args){
        try{
          workers[path]({"data":args});
        }catch(err){
          theworker.onerror(err);
        }
      };
      theworker.postMessage = function(params){
        if(!loaded){
          callings.push(params);
          return;
        }
        theworker.postToWorkerFunction(params);
      };
      masters[path] = theworker;
      var scr = document.createElement("SCRIPT");
      scr.src = path;
      scr.type = "text/javascript";
      scr.onload = function(){
        loaded = true;
        while(callings.length > 0){
          theworker.postToWorkerFunction(callings[0]);
          callings.shift();
        }
      };
      document.body.appendChild(scr);

      var binaryscr = document.createElement("SCRIPT");
      binaryscr.src = thingiurlbase + '/binaryReader.js';
      binaryscr.type = "text/javascript";
      document.body.appendChild(binaryscr);

      return theworker;
    };
    that.fake = true;
    that.add = function(pth, worker){
      workers[pth] = worker;
      return function(param){
        masters[pth].onmessage({"data": param});
      };
    };
    that.toString = function(){
      return "FakeWorker('"+path+"')";
    };
    return that;
  }());
};
