

(function () {
  'use strict';

  function VizScene(container) {
    this.container = container;

    this.renderer = new THREE.WebGLRenderer({
      antialias: true,
      
      
      powerPreference: 'high-performance',
    });
    
    
    
    this._maxPixelRatio = Math.min(window.devicePixelRatio || 1, 1.5);
    this.renderer.setPixelRatio(this._maxPixelRatio);
    this.renderer.outputEncoding = THREE.sRGBEncoding;
    container.appendChild(this.renderer.domElement);

    this.scene = new THREE.Scene();
    this.scene.background = new THREE.Color(0x22262b);

    this.camera = new THREE.PerspectiveCamera(50, 1, 0.01, 50);
    this.camera.position.set(0.65, -0.65, 0.55);
    this.camera.up.set(0, 0, 1);   

    this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
    
    
    
    this.controls.mouseButtons = {
      LEFT: THREE.MOUSE.ROTATE,
      MIDDLE: THREE.MOUSE.PAN,
      RIGHT: null,
    };
    this.controls.target.set(0, 0, 0.25);
    this.controls.update();
    
    
    this.controls.saveState();

    var hemi = new THREE.HemisphereLight(0xffffff, 0x445566, 0.9);
    hemi.position.set(0, 0, 1);
    this.scene.add(hemi);
    var dir = new THREE.DirectionalLight(0xffffff, 0.7);
    dir.position.set(0.5, -0.8, 1.2);
    this.scene.add(dir);

    this.grid = new THREE.GridHelper(2, 20, 0x557799, 0x3a4148);
    this.grid.rotation.x = Math.PI / 2;  
    this.scene.add(this.grid);

    
    
    this.baseFrame = this.makeAxisTriad();
    this.scene.add(this.baseFrame);
    
    
    this.baseLabels = [];
    var AXIS_LABELS = [
      ['X', 0xff0000, new THREE.Vector3(1, 0, 0)],
      ['Y', 0x00ff00, new THREE.Vector3(0, 1, 0)],
      ['Z', 0x0000ff, new THREE.Vector3(0, 0, 1)]
    ];
    for (var ai = 0; ai < AXIS_LABELS.length; ai++) {
      var lab = this._textSprite(AXIS_LABELS[ai][0], AXIS_LABELS[ai][1], 0.055);
      lab.position.copy(AXIS_LABELS[ai][2]).multiplyScalar(DEFAULT_AXIS_LEN * 1.18);
      this.baseFrame.add(lab);
      this.baseLabels.push(lab);
    }

    var self = this;
    window.addEventListener('resize', function () { self.resize(); });
    this.resize();

    this._animCallbacks = [];
    this._initGovernor();
    this._tick = this._tick.bind(this);
    requestAnimationFrame(this._tick);
  }

  












  var TIER_MS = [1000 / 60, 1000 / 30, 1000 / 20];
  var WINDOW_MS = 2000;       
  var LATE_RATIO = 0.20;      
  var QUIET_RATIO = 0.02;     
  var PROMOTE_MS = 30000;     
  var PROMOTE_GROWTH = 3;     
  var MAX_PROMOTE_TRIES = 3;  

  VizScene.prototype._initGovernor = function () {
    this._tier = 0;
    this._tierLimit = null;    
    this._lastRender = 0;
    this._winStart = 0;
    this._winFrames = 0;
    this._winLate = 0;
    this._warmedUp = false;   
    this._promoteWait = PROMOTE_MS;
    this._promoteTries = 0;
    this._quietSince = 0;
    this.frameStats = { tier: 0, fps: 0, lateRatio: 0, tries: 0 };

    









    this._softwareGL = !!(window.__vizGL && window.__vizGL.software);
    if (this._softwareGL) {
      this._tier = TIER_MS.length - 1;
      this._promoteTries = MAX_PROMOTE_TRIES;
      this.frameStats.tier = this._tier;
      this.frameStats.tries = this._promoteTries;
      if (this.renderer && this.renderer.getPixelRatio() !== 1) {
        this.renderer.setPixelRatio(1);
        this.resize();
      }
    }
  };

  VizScene.prototype._applyTier = function (tier) {
    tier = Math.max(0, Math.min(TIER_MS.length - 1, tier));
    if (tier === this._tier) return;
    this._tier = tier;
    
    var pr = (tier >= TIER_MS.length - 1) ? 1 : this._maxPixelRatio;
    if (this.renderer.getPixelRatio() !== pr) {
      this.renderer.setPixelRatio(pr);
      this.resize();
    }
  };

  














  VizScene.prototype.limitTier = function (maxTier) {
    if (maxTier === null || maxTier === undefined) {
      this._tierLimit = null;
      this._initGovernor();     
      return;
    }
    this._tierLimit = maxTier;
    if (this._tier > maxTier) this._applyTier(maxTier);   
    this._winStart = 0;         
    this._winFrames = 0;
    this._winLate = 0;
  };

  
  VizScene.prototype._governFrame = function (t, interval) {
    this._winFrames++;
    
    if (interval > TIER_MS[this._tier] * 1.4) this._winLate++;
    if (!this._winStart) { this._winStart = t; return; }
    if (t - this._winStart < WINDOW_MS) return;

    var fps = this._winFrames / ((t - this._winStart) / 1000);
    var ratio = this._winFrames ? this._winLate / this._winFrames : 0;
    this._winStart = t;
    this._winFrames = 0;
    this._winLate = 0;
    if (!this._warmedUp) { this._warmedUp = true; return; }

    
    var slowest = (this._tierLimit === null || this._tierLimit === undefined)
                  ? TIER_MS.length - 1 : this._tierLimit;

    if (ratio > LATE_RATIO && this._tier < slowest) {
      this._applyTier(this._tier + 1);
      this._quietSince = 0;
    } else if (ratio < QUIET_RATIO && this._tier > 0 &&
               this._promoteTries < MAX_PROMOTE_TRIES) {
      
      
      
      
      if (!this._quietSince) this._quietSince = t;
      else if (t - this._quietSince >= this._promoteWait) {
        this._promoteTries++;
        this._promoteWait *= PROMOTE_GROWTH;
        this._applyTier(this._tier - 1);
        this._quietSince = 0;
      }
    } else {
      this._quietSince = 0;
    }
    this.frameStats = { tier: this._tier, fps: fps, lateRatio: ratio,
                        tries: this._promoteTries };
  };

  var DEFAULT_AXIS_LEN = 0.26;

  


  VizScene.prototype.makeAxisTriad = function (len) {
    len = len || DEFAULT_AXIS_LEN;
    var g = new THREE.Group();
    g.add(this._axisArrow(new THREE.Vector3(1, 0, 0), 0xff0000, len));
    g.add(this._axisArrow(new THREE.Vector3(0, 1, 0), 0x00ff00, len));
    g.add(this._axisArrow(new THREE.Vector3(0, 0, 1), 0x0000ff, len));
    return g;
  };

  

  VizScene.prototype._axisArrow = function (dir, color, len) {
    var LEN = len || DEFAULT_AXIS_LEN;
    var k = LEN / DEFAULT_AXIS_LEN;
    var SHAFT_R = 0.006 * k, HEAD_LEN = 0.05 * k, HEAD_R = 0.017 * k;
    var mat = new THREE.MeshBasicMaterial({ color: color, depthTest: false, depthWrite: false });
    var g = new THREE.Group();
    var shaftLen = LEN - HEAD_LEN;
    var shaft = new THREE.Mesh(new THREE.CylinderGeometry(SHAFT_R, SHAFT_R, shaftLen, 12), mat);
    shaft.position.y = shaftLen / 2;                       
    var head = new THREE.Mesh(new THREE.ConeGeometry(HEAD_R, HEAD_LEN, 16), mat);
    head.position.y = shaftLen + HEAD_LEN / 2;
    g.add(shaft); g.add(head);
    g.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir.clone().normalize());
    g.renderOrder = 999;
    return g;
  };

  
  VizScene.prototype.resetView = function () {
    this.controls.reset();          
    this.camera.up.set(0, 0, 1);    
    this.controls.update();
  };

  VizScene.prototype.setBaseFrameVisible = function (on) {
    if (this.baseFrame) this.baseFrame.visible = !!on;
  };

  VizScene.prototype.isBaseFrameVisible = function () {
    return !!(this.baseFrame && this.baseFrame.visible);
  };

  

  VizScene.prototype._textSprite = function (text, color, sizeAt1M) {
    
    
    function cssColor(c) {
      if (typeof c === 'number') {
        return '#' + ('000000' + c.toString(16)).slice(-6);
      }
      return c || '#c5c8c6';
    }
    var FONT = 64;
    var dpr = Math.min(window.devicePixelRatio || 1, 2);
    var pad = Math.round(FONT * 0.3);
    var measure = document.createElement('canvas').getContext('2d');
    measure.font = 'bold ' + FONT + 'px sans-serif';
    var textW = Math.ceil(measure.measureText(text).width);
    var cw = textW + pad * 2, ch = Math.round(FONT * 1.4);
    var cv = document.createElement('canvas');
    cv.width = Math.round(cw * dpr);
    cv.height = Math.round(ch * dpr);
    var ctx = cv.getContext('2d');
    ctx.scale(dpr, dpr);
    ctx.font = 'bold ' + FONT + 'px sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillStyle = cssColor(color);
    ctx.fillText(text, cw / 2, ch / 2);
    var tex = new THREE.CanvasTexture(cv);
    tex.minFilter = THREE.LinearFilter;
    tex.generateMipmaps = false;
    var sp = new THREE.Sprite(new THREE.SpriteMaterial({
      map: tex, transparent: true, depthTest: false, depthWrite: false
    }));
    sp.renderOrder = 999;
    sp.userData = { sizeAt1M: sizeAt1M || 0.05, aspect: cw / ch };
    return sp;
  };

  
  VizScene.prototype._updateLabels = function () {
    var cam = this.camera;
    function scale(sp) {
      if (!sp.visible || !sp.userData) return;
      var d = sp.position.distanceTo(cam.position) || 1;
      var h = sp.userData.sizeAt1M * d;
      sp.scale.set(h * sp.userData.aspect, h, 1);
    }
    if (this.baseFrame && this.baseFrame.visible) {
      for (var i = 0; i < this.baseLabels.length; i++) scale(this.baseLabels[i]);
    }
  };

  VizScene.prototype.resize = function () {
    var w = this.container.clientWidth || 1;
    var h = this.container.clientHeight || 1;
    this.camera.aspect = w / h;
    this.camera.updateProjectionMatrix();
    this.renderer.setSize(w, h);
  };

  VizScene.prototype.onFrame = function (cb) { this._animCallbacks.push(cb); };

  

  VizScene.prototype.onFrameFirst = function (cb) { this._animCallbacks.unshift(cb); };

  


  VizScene.prototype._tick = function (t) {
    requestAnimationFrame(this._tick);      
    var interval = this._lastRender ? t - this._lastRender : 0;
    
    if (this._lastRender && interval < TIER_MS[this._tier] - 1) return;
    this._lastRender = t;
    for (var i = 0; i < this._animCallbacks.length; i++) this._animCallbacks[i](t);
    this._updateLabels();
    this.renderer.render(this.scene, this.camera);
    this._governFrame(t, interval);
  };

  window.VizScene = VizScene;
})();
