text: /**
   * dat-gui JavaScript Controller Library
   * http://code.google.com/p/dat-gui
   *
   * Copyright 2011 Data Arts Team, Google Creative Lab
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   * http://www.apache.org/licenses/LICENSE-2.0
   */
  
  import css from '../utils/css';
  import saveDialogueContents from './saveDialogue.html';
  import ControllerFactory from '../controllers/ControllerFactory';
  import Controller from '../controllers/Controller';
  import BooleanController from '../controllers/BooleanController';
  import FunctionController from '../controllers/FunctionController';
  import NumberControllerBox from '../controllers/NumberControllerBox';
  import NumberControllerSlider from '../controllers/NumberControllerSlider';
  import ColorController from '../controllers/ColorController';
  import requestAnimationFrame from '../utils/requestAnimationFrame';
  import CenteredDiv from '../dom/CenteredDiv';
  import dom from '../dom/dom';
  import common from '../utils/common';
  
  import styleSheet from './style.scss'; // CSS to embed in build
  
  css.inject(styleSheet);
  
  /** @ignore Outer-most className for GUI's */
  const CSS_NAMESPACE = 'dg';
  
  const HIDE_KEY_CODE = 72;
  
  /** @ignore The only value shared between the JS and SCSS. Use caution. */
  const CLOSE_BUTTON_HEIGHT = 20;
  
  const DEFAULT_DEFAULT_PRESET_NAME = 'Default';
  
  const SUPPORTS_LOCAL_STORAGE = (function() {
    try {
      return 'localStorage' in window && window.localStorage !== null;
    } catch (e) {
      return false;
    }
  }());
  
  let SAVE_DIALOGUE;
  
  /** @ignore Have we yet to create an autoPlace GUI? */
  let autoPlaceVirgin = true;
  
  /** @ignore Fixed position div that auto place GUI's go inside */
  let autoPlaceContainer;
  
  /** @ignore Are we hiding the GUI's ? */
  let hide = false;
  
  /** @private GUI's which should be hidden */
  const hideableGuis = [];
  
  /**
   * @class A lightweight controller library for JavaScript. It allows you to easily
   * manipulate variables and fire functions on the fly.
   *
   * @typicalname gui
   *
   * @example
   * // Creating a GUI with options.
   * var gui = new dat.GUI({name: 'My GUI'});
   *
   * @example
   * // Creating a GUI and a subfolder.
   * var gui = new dat.GUI();
   * var folder1 = gui.addFolder('Flow Field');
   *
   * @param {Object} [params]
   * @param {String} [params.name] The name of this GUI.
   * @param {Object} [params.load] JSON object representing the saved state of
   * this GUI.
   * @param {Boolean} [params.auto=true]
   * @param {dat.gui.GUI} [params.parent] The GUI I'm nested in.
   * @param {Boolean} [params.closed] If true, starts closed
   * @param {Boolean} [params.closeOnTop] If true, close/open button shows on top of the GUI
   */
  const GUI = function(pars) {
    const _this = this;
  
    let params = pars || {};
  
    /**
     * Outermost DOM Element
     * @type {DOMElement}
     */
    this.domElement = document.createElement('div');
    this.__ul = document.createElement('ul');
    this.domElement.appendChild(this.__ul);
  
    dom.addClass(this.domElement, CSS_NAMESPACE);
  
    /**
     * Nested GUI's by name
     * @ignore
     */
    this.__folders = {};
  
    this.__controllers = [];
  
    /**
     * List of objects I'm remembering for save, only used in top level GUI
     * @ignore
     */
    this.__rememberedObjects = [];
  
    /**
     * Maps the index of remembered objects to a map of controllers, only used
     * in top level GUI.
     *
     * @private
     * @ignore
     *
     * @example
     * [
     *  {
       *    propertyName: Controller,
       *    anotherPropertyName: Controller
       *  },
     *  {
       *    propertyName: Controller
       *  }
     * ]
     */
    this.__rememberedObjectIndecesToControllers = [];
  
    this.__listening = [];
  
    // Default parameters
    params = common.defaults(params, {
      closeOnTop: false,
      autoPlace: true,
      width: GUI.DEFAULT_WIDTH
    });
  
    params = common.defaults(params, {
      resizable: params.autoPlace,
      hideable: params.autoPlace
    });
  
    if (!common.isUndefined(params.load)) {
      // Explicit preset
      if (params.preset) {
        params.load.preset = params.preset;
      }
    } else {
      params.load = { preset: DEFAULT_DEFAULT_PRESET_NAME };
    }
  
    if (common.isUndefined(params.parent) && params.hideable) {
      hideableGuis.push(this);
    }
  
    // Only root level GUI's are resizable.
    params.resizable = common.isUndefined(params.parent) && params.resizable;
  
    if (params.autoPlace && common.isUndefined(params.scrollable)) {
      params.scrollable = true;
    }
  //    params.scrollable = common.isUndefined(params.parent) && params.scrollable === true;
  
    // Not part of params because I don't want people passing this in via
    // constructor. Should be a 'remembered' value.
    let useLocalStorage =
      SUPPORTS_LOCAL_STORAGE &&
      localStorage.getItem(getLocalStorageHash(this, 'isLocal')) === 'true';
  
    let saveToLocalStorage;
  
    Object.defineProperties(this,
      /** @lends GUI.prototype */
      {
        /**
         * The parent <code>GUI</code>
         * @type dat.gui.GUI
         */
        parent: {
          get: function() {
            return params.parent;
          }
        },
  
        scrollable: {
          get: function() {
            return params.scrollable;
          }
        },
  
        /**
         * Handles <code>GUI</code>'s element placement for you
         * @type Boolean
         */
        autoPlace: {
          get: function() {
            return params.autoPlace;
          }
        },
  
        /**
         * Handles <code>GUI</code>'s position of open/close button
         * @type Boolean
         */
        closeOnTop: {
          get: function() {
            return params.closeOnTop;
          }
        },
  
        /**
         * The identifier for a set of saved values
         * @type String
         */
        preset: {
          get: function() {
            if (_this.parent) {
              return _this.getRoot().preset;
            }
  
            return params.load.preset;
          },
  
          set: function(v) {
            if (_this.parent) {
              _this.getRoot().preset = v;
            } else {
              params.load.preset = v;
            }
            setPresetSelectIndex(this);
            _this.revert();
          }
        },
  
        /**
         * The width of <code>GUI</code> element
         * @type Number
         */
        width: {
          get: function() {
            return params.width;
          },
          set: function(v) {
            params.width = v;
            setWidth(_this, v);
          }
        },
  
        /**
         * The name of <code>GUI</code>. Used for folders. i.e
         * a folder's name
         * @type String
         */
        name: {
          get: function() {
            return params.name;
          },
          set: function(v) {
            // TODO Check for collisions among sibling folders
            params.name = v;
            if (titleRowName) {
              titleRowName.innerHTML = params.name;
            }
          }
        },
  
        /**
         * Whether the <code>GUI</code> is collapsed or not
         * @type Boolean
         */
        closed: {
          get: function() {
            return params.closed;
          },
          set: function(v) {
            params.closed = v;
            if (params.closed) {
              dom.addClass(_this.__ul, GUI.CLASS_CLOSED);
            } else {
              dom.removeClass(_this.__ul, GUI.CLASS_CLOSED);
            }
            // For browsers that aren't going to respect the CSS transition,
            // Lets just check our height against the window height right off
            // the bat.
            this.onResize();
  
            if (_this.__closeButton) {
              _this.__closeButton.innerHTML = v ? GUI.TEXT_OPEN : GUI.TEXT_CLOSED;
            }
          }
        },
  
        /**
         * Contains all presets
         * @type Object
         */
        load: {
          get: function() {
            return params.load;
          }
        },
  
        /**
         * Determines whether or not to use <a href="https://developer.mozilla.org/en/DOM/Storage#localStorage">localStorage</a> as the means for
         * <code>remember</code>ing
         * @type Boolean
         */
        useLocalStorage: {
  
          get: function() {
            return useLocalStorage;
          },
          set: function(bool) {
            if (SUPPORTS_LOCAL_STORAGE) {
              useLocalStorage = bool;
              if (bool) {
                dom.bind(window, 'unload', saveToLocalStorage);
              } else {
                dom.unbind(window, 'unload', saveToLocalStorage);
              }
              localStorage.setItem(getLocalStorageHash(_this, 'isLocal'), bool);
            }
          }
        }
      });
  
    // Are we a root level GUI?
    if (common.isUndefined(params.parent)) {
      params.closed = false;
  
      dom.addClass(this.domElement, GUI.CLASS_MAIN);
      dom.makeSelectable(this.domElement, false);
  
      // Are we supposed to be loading locally?
      if (SUPPORTS_LOCAL_STORAGE) {
        if (useLocalStorage) {
          _this.useLocalStorage = true;
  
          const savedGui = localStorage.getItem(getLocalStorageHash(this, 'gui'));
  
          if (savedGui) {
            params.load = JSON.parse(savedGui);
          }
        }
      }
  
      this.__closeButton = document.createElement('div');
      this.__closeButton.innerHTML = GUI.TEXT_CLOSED;
      dom.addClass(this.__closeButton, GUI.CLASS_CLOSE_BUTTON);
      if (params.closeOnTop) {
        dom.addClass(this.__closeButton, GUI.CLASS_CLOSE_TOP);
        this.domElement.insertBefore(this.__closeButton, this.domElement.childNodes[0]);
      } else {
        dom.addClass(this.__closeButton, GUI.CLASS_CLOSE_BOTTOM);
        this.domElement.appendChild(this.__closeButton);
      }
  
      dom.bind(this.__closeButton, 'click', function() {
        _this.closed = !_this.closed;
      });
      // Oh, you're a nested GUI!
    } else {
      if (params.closed === undefined) {
        params.closed = true;
      }
  
      const titleRowName = document.createTextNode(params.name);
      dom.addClass(titleRowName, 'controller-name');
  
      const titleRow = addRow(_this, titleRowName);
  
      const onClickTitle = function(e) {
        e.preventDefault();
        _this.closed = !_this.closed;
        return false;
      };
  
      dom.addClass(this.__ul, GUI.CLASS_CLOSED);
  
      dom.addClass(titleRow, 'title');
      dom.bind(titleRow, 'click', onClickTitle);
  
      if (!params.closed) {
        this.closed = false;
      }
    }
  
    if (params.autoPlace) {
      if (common.isUndefined(params.parent)) {
        if (autoPlaceVirgin) {
          autoPlaceContainer = document.createElement('div');
          dom.addClass(autoPlaceContainer, CSS_NAMESPACE);
          dom.addClass(autoPlaceContainer, GUI.CLASS_AUTO_PLACE_CONTAINER);
          document.body.appendChild(autoPlaceContainer);
          autoPlaceVirgin = false;
        }
  
        // Put it in the dom for you.
        autoPlaceContainer.appendChild(this.domElement);
  
        // Apply the auto styles
        dom.addClass(this.domElement, GUI.CLASS_AUTO_PLACE);
      }
  
  
      // Make it not elastic.
      if (!this.parent) {
        setWidth(_this, params.width);
      }
    }
  
    this.__resizeHandler = function() {
      _this.onResizeDebounced();
    };
  
    dom.bind(window, 'resize', this.__resizeHandler);
    dom.bind(this.__ul, 'webkitTransitionEnd', this.__resizeHandler);
    dom.bind(this.__ul, 'transitionend', this.__resizeHandler);
    dom.bind(this.__ul, 'oTransitionEnd', this.__resizeHandler);
    this.onResize();
  
    if (params.resizable) {
      addResizeHandle(this);
    }
  
    saveToLocalStorage = function() {
      if (SUPPORTS_LOCAL_STORAGE && localStorage.getItem(getLocalStorageHash(_this, 'isLocal')) === 'true') {
        localStorage.setItem(getLocalStorageHash(_this, 'gui'), JSON.stringify(_this.getSaveObject()));
      }
    };
  
    // expose this method publicly
    this.saveToLocalStorageIfPossible = saveToLocalStorage;
  
    function resetWidth() {
      const root = _this.getRoot();
      root.width += 1;
      common.defer(function() {
        root.width -= 1;
      });
    }
  
    if (!params.parent) {
      resetWidth();
    }
  };
  
  GUI.toggleHide = function() {
    hide = !hide;
    common.each(hideableGuis, function(gui) {
      gui.domElement.style.display = hide ? 'none' : '';
    });
  };
  
  GUI.CLASS_AUTO_PLACE = 'a';
  GUI.CLASS_AUTO_PLACE_CONTAINER = 'ac';
  GUI.CLASS_MAIN = 'main';
  GUI.CLASS_CONTROLLER_ROW = 'cr';
  GUI.CLASS_TOO_TALL = 'taller-than-window';
  GUI.CLASS_CLOSED = 'closed';
  GUI.CLASS_CLOSE_BUTTON = 'close-button';
  GUI.CLASS_CLOSE_TOP = 'close-top';
  GUI.CLASS_CLOSE_BOTTOM = 'close-bottom';
  GUI.CLASS_DRAG = 'drag';
  
  GUI.DEFAULT_WIDTH = 245;
  GUI.TEXT_CLOSED = 'Close Controls';
  GUI.TEXT_OPEN = 'Open Controls';
  
  GUI._keydownHandler = function(e) {
    if (document.activeElement.type !== 'text' &&
      (e.which === HIDE_KEY_CODE || e.keyCode === HIDE_KEY_CODE)) {
      GUI.toggleHide();
    }
  };
  dom.bind(window, 'keydown', GUI._keydownHandler, false);
  
  common.extend(
    GUI.prototype,
  
    /** @lends GUI.prototype */
    {
  
      /**
       * Adds a new {@link Controller} to the GUI. The type of controller created
       * is inferred from the initial value of <code>object[property]</code>. For
       * color properties, see {@link addColor}.
       *
       * @param {Object} object The object to be manipulated
       * @param {String} property The name of the property to be manipulated
       * @param {Number} [min] Minimum allowed value
       * @param {Number} [max] Maximum allowed value
       * @param {Number} [step] Increment by which to change value
       * @returns {Controller} The controller that was added to the GUI.
       * @instance
       *
       * @example
       * // Add a string controller.
       * var person = {name: 'Sam'};
       * gui.add(person, 'name');
       *
       * @example
       * // Add a number controller slider.
       * var person = {age: 45};
       * gui.add(person, 'age', 0, 100);
       */
      add: function(object, property) {
        return add(
          this,
          object,
          property,
          {
            factoryArgs: Array.prototype.slice.call(arguments, 2)
          }
        );
      },
  
      /**
       * Adds a new color controller to the GUI.
       *
       * @param object
       * @param property
       * @returns {Controller} The controller that was added to the GUI.
       * @instance
       *
       * @example
       * var palette = {
       *   color1: '#FF0000', // CSS string
       *   color2: [ 0, 128, 255 ], // RGB array
       *   color3: [ 0, 128, 255, 0.3 ], // RGB with alpha
       *   color4: { h: 350, s: 0.9, v: 0.3 } // Hue, saturation, value
       * };
       * gui.addColor(palette, 'color1');
       * gui.addColor(palette, 'color2');
       * gui.addColor(palette, 'color3');
       * gui.addColor(palette, 'color4');
       */
      addColor: function(object, property) {
        return add(
          this,
          object,
          property,
          {
            color: true
          }
        );
      },
  
      /**
       * Removes the given controller from the GUI.
       * @param {Controller} controller
       * @instance
       */
      remove: function(controller) {
        // TODO listening?
        this.__ul.removeChild(controller.__li);
        this.__controllers.splice(this.__controllers.indexOf(controller), 1);
        const _this = this;
        common.defer(function() {
          _this.onResize();
        });
      },
  
      /**
       * Removes the GUI from the document and unbinds all event listeners.
       * @instance
       */
      destroy: function() {
        if (this.autoPlace) {
          autoPlaceContainer.removeChild(this.domElement);
        }
  
        dom.unbind(window, 'keydown', GUI._keydownHandler, false);
        dom.unbind(window, 'resize', this.__resizeHandler);
  
        if (this.saveToLocalStorageIfPossible) {
          dom.unbind(window, 'unload', this.saveToLocalStorageIfPossible);
        }
      },
  
      /**
       * Creates a new subfolder GUI instance.
       * @param name
       * @returns {dat.gui.GUI} The new folder.
       * @throws {Error} if this GUI already has a folder by the specified
       * name
       * @instance
       */
      addFolder: function(name) {
        // We have to prevent collisions on names in order to have a key
        // by which to remember saved values
        if (this.__folders[name] !== undefined) {
          throw new Error('You already have a folder in this GUI by the' +
            ' name "' + name + '"');
        }
  
        const newGuiParams = { name: name, parent: this };
  
        // We need to pass down the autoPlace trait so that we can
        // attach event listeners to open/close folder actions to
        // ensure that a scrollbar appears if the window is too short.
        newGuiParams.autoPlace = this.autoPlace;
  
        // Do we have saved appearance data for this folder?
        if (this.load && // Anything loaded?
          this.load.folders && // Was my parent a dead-end?
          this.load.folders[name]) { // Did daddy remember me?
          // Start me closed if I was closed
          newGuiParams.closed = this.load.folders[name].closed;
  
          // Pass down the loaded data
          newGuiParams.load = this.load.folders[name];
        }
  
        const gui = new GUI(newGuiParams);
        this.__folders[name] = gui;
  
        const li = addRow(this, gui.domElement);
        dom.addClass(li, 'folder');
        return gui;
      },
  
      /**
       * Removes a subfolder GUI instance.
       * @param {dat.gui.GUI} folder The folder to remove.
       * @instance
       */
      removeFolder: function(folder) {
        this.__ul.removeChild(folder.domElement.parentElement);
  
        delete this.__folders[folder.name];
  
        // Do we have saved appearance data for this folder?
        if (this.load && // Anything loaded?
          this.load.folders && // Was my parent a dead-end?
          this.load.folders[folder.name]) {
          delete this.load.folders[folder.name];
        }
  
        const _this = this;
        common.defer(function() {
          _this.onResize();
        });
      },
  
      /**
       * Opens the GUI.
       */
      open: function() {
        this.closed = false;
      },
  
      /**
       * Closes the GUI.
       */
      close: function() {
        this.closed = true;
      },
  
  
      onResize: function() {
        // we debounce this function to prevent performance issues when rotating on tablet/mobile
        const root = this.getRoot();
        if (root.scrollable) {
          const top = dom.getOffset(root.__ul).top;
          let h = 0;
  
          common.each(root.__ul.childNodes, function(node) {
            if (!(root.autoPlace && node === root.__save_row)) {
              h += dom.getHeight(node);
            }
          });
  
          if (window.innerHeight - top - CLOSE_BUTTON_HEIGHT < h) {
            dom.addClass(root.domElement, GUI.CLASS_TOO_TALL);
            root.__ul.style.height = window.innerHeight - top - CLOSE_BUTTON_HEIGHT + 'px';
          } else {
            dom.removeClass(root.domElement, GUI.CLASS_TOO_TALL);
            root.__ul.style.height = 'auto';
          }
        }
  
        if (root.__resize_handle) {
          common.defer(function() {
            root.__resize_handle.style.height = root.__ul.offsetHeight + 'px';
          });
        }
  
        if (root.__closeButton) {
          root.__closeButton.style.width = root.width + 'px';
        }
      },
  
      onResizeDebounced: common.debounce(function() { this.onResize(); }, 50),
  
      /**
       * Mark objects for saving. The order of these objects cannot change as
       * the GUI grows. When remembering new objects, append them to the end
       * of the list.
       *
       * @param {...Object} objects
       * @throws {Error} if not called on a top level GUI.
       * @instance
       * @ignore
       */
      remember: function() {
        if (common.isUndefined(SAVE_DIALOGUE)) {
          SAVE_DIALOGUE = new CenteredDiv();
          SAVE_DIALOGUE.domElement.innerHTML = saveDialogueContents;
        }
  
        if (this.parent) {
          throw new Error('You can only call remember on a top level GUI.');
        }
  
        const _this = this;
  
        common.each(Array.prototype.slice.call(arguments), function(object) {
          if (_this.__rememberedObjects.length === 0) {
            addSaveMenu(_this);
          }
          if (_this.__rememberedObjects.indexOf(object) === -1) {
            _this.__rememberedObjects.push(object);
          }
        });
  
        if (this.autoPlace) {
          // Set save row width
          setWidth(this, this.width);
        }
      },
  
      /**
       * @returns {dat.gui.GUI} the topmost parent GUI of a nested GUI.
       * @instance
       */
      getRoot: function() {
        let gui = this;
        while (gui.parent) {
          gui = gui.parent;
        }
        return gui;
      },
  
      /**
       * @returns {Object} a JSON object representing the current state of
       * this GUI as well as its remembered properties.
       * @instance
       */
      getSaveObject: function() {
        const toReturn = this.load;
        toReturn.closed = this.closed;
  
        // Am I remembering any values?
        if (this.__rememberedObjects.length > 0) {
          toReturn.preset = this.preset;
  
          if (!toReturn.remembered) {
            toReturn.remembered = {};
          }
  
          toReturn.remembered[this.preset] = getCurrentPreset(this);
        }
  
        toReturn.folders = {};
        common.each(this.__folders, function(element, key) {
          toReturn.folders[key] = element.getSaveObject();
        });
  
        return toReturn;
      },
  
      save: function() {
        if (!this.load.remembered) {
          this.load.remembered = {};
        }
  
        this.load.remembered[this.preset] = getCurrentPreset(this);
        markPresetModified(this, false);
        this.saveToLocalStorageIfPossible();
      },
  
      saveAs: function(presetName) {
        if (!this.load.remembered) {
          // Retain default values upon first save
          this.load.remembered = {};
          this.load.remembered[DEFAULT_DEFAULT_PRESET_NAME] = getCurrentPreset(this, true);
        }
  
        this.load.remembered[presetName] = getCurrentPreset(this);
        this.preset = presetName;
        addPresetOption(this, presetName, true);
        this.saveToLocalStorageIfPossible();
      },
  
      revert: function(gui) {
        common.each(this.__controllers, function(controller) {
          // Make revert work on Default.
          if (!this.getRoot().load.remembered) {
            controller.setValue(controller.initialValue);
          } else {
            recallSavedValue(gui || this.getRoot(), controller);
          }
  
          // fire onFinishChange callback
          if (controller.__onFinishChange) {
            controller.__onFinishChange.call(controller, controller.getValue());
          }
        }, this);
  
        common.each(this.__folders, function(folder) {
          folder.revert(folder);
        });
  
        if (!gui) {
          markPresetModified(this.getRoot(), false);
        }
      },
  
      listen: function(controller) {
        const init = this.__listening.length === 0;
        this.__listening.push(controller);
        if (init) {
          updateDisplays(this.__listening);
        }
      },
  
      updateDisplay: function() {
        common.each(this.__controllers, function(controller) {
          controller.updateDisplay();
        });
        common.each(this.__folders, function(folder) {
          folder.updateDisplay();
        });
      }
    }
  );
  
  /**
   * Add a row to the end of the GUI or before another row.
   *
   * @param gui
   * @param [newDom] If specified, inserts the dom content in the new row
   * @param [liBefore] If specified, places the new row before another row
   *
   * @ignore
   */
  function addRow(gui, newDom, liBefore) {
    const li = document.createElement('li');
    if (newDom) {
      li.appendChild(newDom);
    }
  
    if (liBefore) {
      gui.__ul.insertBefore(li, liBefore);
    } else {
      gui.__ul.appendChild(li);
    }
    gui.onResize();
    return li;
  }
  
  function markPresetModified(gui, modified) {
    const opt = gui.__preset_select[gui.__preset_select.selectedIndex];
  
    // console.log('mark', modified, opt);
    if (modified) {
      opt.innerHTML = opt.value + '*';
    } else {
      opt.innerHTML = opt.value;
    }
  }
  
  function augmentController(gui, li, controller) {
    controller.__li = li;
    controller.__gui = gui;
  
    common.extend(controller, /** @lends Controller.prototype */ {
      /**
       * @param  {Array|Object} options
       * @return {Controller}
       */
      options: function(options) {
        if (arguments.length > 1) {
          const nextSibling = controller.__li.nextElementSibling;
          controller.remove();
  
          return add(
            gui,
            controller.object,
            controller.property,
            {
              before: nextSibling,
              factoryArgs: [common.toArray(arguments)]
            }
          );
        }
  
        if (common.isArray(options) || common.isObject(options)) {
          const nextSibling = controller.__li.nextElementSibling;
          controller.remove();
  
          return add(
            gui,
            controller.object,
            controller.property,
            {
              before: nextSibling,
              factoryArgs: [options]
            }
          );
        }
      },
  
      /**
       * Sets the name of the controller.
       * @param  {string} name
       * @return {Controller}
       */
      name: function(name) {
        controller.__li.firstElementChild.firstElementChild.innerHTML = name;
        return controller;
      },
  
      /**
       * Sets controller to listen for changes on its underlying object.
       * @return {Controller}
       */
      listen: function() {
        controller.__gui.listen(controller);
        return controller;
      },
  
      /**
       * Removes the controller from its parent GUI.
       * @return {Controller}
       */
      remove: function() {
        controller.__gui.remove(controller);
        return controller;
      }
    });
  
    // All sliders should be accompanied by a box.
    if (controller instanceof NumberControllerSlider) {
      const box = new NumberControllerBox(controller.object, controller.property,
        { min: controller.__min, max: controller.__max, step: controller.__step });
  
      common.each(['updateDisplay', 'onChange', 'onFinishChange', 'step'], function(method) {
        const pc = controller[method];
        const pb = box[method];
        controller[method] = box[method] = function() {
          const args = Array.prototype.slice.call(arguments);
          pb.apply(box, args);
          return pc.apply(controller, args);
        };
      });
  
      dom.addClass(li, 'has-slider');
      controller.domElement.insertBefore(box.domElement, controller.domElement.firstElementChild);
    } else if (controller instanceof NumberControllerBox) {
      const r = function(returned) {
        // Have we defined both boundaries?
        if (common.isNumber(controller.__min) && common.isNumber(controller.__max)) {
          // Well, then lets just replace this with a slider.
  
          // lets remember if the old controller had a specific name or was listening
          const oldName = controller.__li.firstElementChild.firstElementChild.innerHTML;
          const wasListening = controller.__gui.__listening.indexOf(controller) > -1;
  
          controller.remove();
          const newController = add(
            gui,
            controller.object,
            controller.property,
            {
              before: controller.__li.nextElementSibling,
              factoryArgs: [controller.__min, controller.__max, controller.__step]
            });
  
          newController.name(oldName);
          if (wasListening) newController.listen();
  
          return newController;
        }
  
        return returned;
      };
  
      controller.min = common.compose(r, controller.min);
      controller.max = common.compose(r, controller.max);
    } else if (controller instanceof BooleanController) {
      dom.bind(li, 'click', function() {
        dom.fakeEvent(controller.__checkbox, 'click');
      });
  
      dom.bind(controller.__checkbox, 'click', function(e) {
        e.stopPropagation(); // Prevents double-toggle
      });
    } else if (controller instanceof FunctionController) {
      dom.bind(li, 'click', function() {
        dom.fakeEvent(controller.__button, 'click');
      });
  
      dom.bind(li, 'mouseover', function() {
        dom.addClass(controller.__button, 'hover');
      });
  
      dom.bind(li, 'mouseout', function() {
        dom.removeClass(controller.__button, 'hover');
      });
    } else if (controller instanceof ColorController) {
      dom.addClass(li, 'color');
      controller.updateDisplay = common.compose(function(val) {
        li.style.borderLeftColor = controller.__color.toString();
        return val;
      }, controller.updateDisplay);
  
      controller.updateDisplay();
    }
  
    controller.setValue = common.compose(function(val) {
      if (gui.getRoot().__preset_select && controller.isModified()) {
        markPresetModified(gui.getRoot(), true);
      }
  
      return val;
    }, controller.setValue);
  }
  
  function recallSavedValue(gui, controller) {
    // Find the topmost GUI, that's where remembered objects live.
    const root = gui.getRoot();
  
    // Does the object we're controlling match anything we've been told to
    // remember?
    const matchedIndex = root.__rememberedObjects.indexOf(controller.object);
  
    // Why yes, it does!
    if (matchedIndex !== -1) {
      // Let me fetch a map of controllers for thcommon.isObject.
      let controllerMap = root.__rememberedObjectIndecesToControllers[matchedIndex];
  
      // Ohp, I believe this is the first controller we've created for this
      // object. Lets make the map fresh.
      if (controllerMap === undefined) {
        controllerMap = {};
        root.__rememberedObjectIndecesToControllers[matchedIndex] =
          controllerMap;
      }
  
      // Keep track of this controller
      controllerMap[controller.property] = controller;
  
      // Okay, now have we saved any values for this controller?
      if (root.load && root.load.remembered) {
        const presetMap = root.load.remembered;
  
        // Which preset are we trying to load?
        let preset;
  
        if (presetMap[gui.preset]) {
          preset = presetMap[gui.preset];
        } else if (presetMap[DEFAULT_DEFAULT_PRESET_NAME]) {
          // Uhh, you can have the default instead?
          preset = presetMap[DEFAULT_DEFAULT_PRESET_NAME];
        } else {
          // Nada.
          return;
        }
  
        // Did the loaded object remember thcommon.isObject? &&  Did we remember this particular property?
        if (preset[matchedIndex] && preset[matchedIndex][controller.property] !== undefined) {
          // We did remember something for this guy ...
          const value = preset[matchedIndex][controller.property];
  
          // And that's what it is.
          controller.initialValue = value;
          controller.setValue(value);
        }
      }
    }
  }
  
  function add(gui, object, property, params) {
    if (object[property] === undefined) {
      throw new Error(`Object "${object}" has no property "${property}"`);
    }
  
    let controller;
  
    if (params.color) {
      controller = new ColorController(object, property);
    } else {
      const factoryArgs = [object, property].concat(params.factoryArgs);
      controller = ControllerFactory.apply(gui, factoryArgs);
    }
  
    if (params.before instanceof Controller) {
      params.before = params.before.__li;
    }
  
    recallSavedValue(gui, controller);
  
    dom.addClass(controller.domElement, 'c');
  
    const name = document.createElement('span');
    dom.addClass(name, 'property-name');
    name.innerHTML = controller.property;
  
    const container = document.createElement('div');
    container.appendChild(name);
    container.appendChild(controller.domElement);
  
    const li = addRow(gui, container, params.before);
  
    dom.addClass(li, GUI.CLASS_CONTROLLER_ROW);
    if (controller instanceof ColorController) {
      dom.addClass(li, 'color');
    } else {
      dom.addClass(li, typeof controller.getValue());
    }
  
    augmentController(gui, li, controller);
  
    gui.__controllers.push(controller);
  
    return controller;
  }
  
  function getLocalStorageHash(gui, key) {
    // TODO how does this deal with multiple GUI's?
    return document.location.href + '.' + key;
  }
  
  function addPresetOption(gui, name, setSelected) {
    const opt = document.createElement('option');
    opt.innerHTML = name;
    opt.value = name;
    gui.__preset_select.appendChild(opt);
    if (setSelected) {
      gui.__preset_select.selectedIndex = gui.__preset_select.length - 1;
    }
  }
  
  function showHideExplain(gui, explain) {
    explain.style.display = gui.useLocalStorage ? 'block' : 'none';
  }
  
  function addSaveMenu(gui) {
    const div = gui.__save_row = document.createElement('li');
  
    dom.addClass(gui.domElement, 'has-save');
  
    gui.__ul.insertBefore(div, gui.__ul.firstChild);
  
    dom.addClass(div, 'save-row');
  
    const gears = document.createElement('span');
    gears.innerHTML = '&nbsp;';
    dom.addClass(gears, 'button gears');
  
    // TODO replace with FunctionController
    const button = document.createElement('span');
    button.innerHTML = 'Save';
    dom.addClass(button, 'button');
    dom.addClass(button, 'save');
  
    const button2 = document.createElement('span');
    button2.innerHTML = 'New';
    dom.addClass(button2, 'button');
    dom.addClass(button2, 'save-as');
  
    const button3 = document.createElement('span');
    button3.innerHTML = 'Revert';
    dom.addClass(button3, 'button');
    dom.addClass(button3, 'revert');
  
    const select = gui.__preset_select = document.createElement('select');
  
    if (gui.load && gui.load.remembered) {
      common.each(gui.load.remembered, function(value, key) {
        addPresetOption(gui, key, key === gui.preset);
      });
    } else {
      addPresetOption(gui, DEFAULT_DEFAULT_PRESET_NAME, false);
    }
  
    dom.bind(select, 'change', function() {
      for (let index = 0; index < gui.__preset_select.length; index++) {
        gui.__preset_select[index].innerHTML = gui.__preset_select[index].value;
      }
  
      gui.preset = this.value;
    });
  
    div.appendChild(select);
    div.appendChild(gears);
    div.appendChild(button);
    div.appendChild(button2);
    div.appendChild(button3);
  
    if (SUPPORTS_LOCAL_STORAGE) {
      const explain = document.getElementById('dg-local-explain');
      const localStorageCheckBox = document.getElementById('dg-local-storage');
      const saveLocally = document.getElementById('dg-save-locally');
  
      saveLocally.style.display = 'block';
  
      if (localStorage.getItem(getLocalStorageHash(gui, 'isLocal')) === 'true') {
        localStorageCheckBox.setAttribute('checked', 'checked');
      }
  
      showHideExplain(gui, explain);
  
      // TODO: Use a boolean controller, fool!
      dom.bind(localStorageCheckBox, 'change', function() {
        gui.useLocalStorage = !gui.useLocalStorage;
        showHideExplain(gui, explain);
      });
    }
  
    const newConstructorTextArea = document.getElementById('dg-new-constructor');
  
    dom.bind(newConstructorTextArea, 'keydown', function(e) {
      if (e.metaKey && (e.which === 67 || e.keyCode === 67)) {
        SAVE_DIALOGUE.hide();
      }
    });
  
    dom.bind(gears, 'click', function() {
      newConstructorTextArea.innerHTML = JSON.stringify(gui.getSaveObject(), undefined, 2);
      SAVE_DIALOGUE.show();
      newConstructorTextArea.focus();
      newConstructorTextArea.select();
    });
  
    dom.bind(button, 'click', function() {
      gui.save();
    });
  
    dom.bind(button2, 'click', function() {
      const presetName = prompt('Enter a new preset name.');
      if (presetName) {
        gui.saveAs(presetName);
      }
    });
  
    dom.bind(button3, 'click', function() {
      gui.revert();
    });
  
    // div.appendChild(button2);
  }
  
  function addResizeHandle(gui) {
    let pmouseX;
  
    gui.__resize_handle = document.createElement('div');
  
    common.extend(gui.__resize_handle.style, {
  
      width: '6px',
      marginLeft: '-3px',
      height: '200px',
      cursor: 'ew-resize',
      position: 'absolute'
      // border: '1px solid blue'
  
    });
  
    function drag(e) {
      e.preventDefault();
  
      gui.width += pmouseX - e.clientX;
      gui.onResize();
      pmouseX = e.clientX;
  
      return false;
    }
  
    function dragStop() {
      dom.removeClass(gui.__closeButton, GUI.CLASS_DRAG);
      dom.unbind(window, 'mousemove', drag);
      dom.unbind(window, 'mouseup', dragStop);
    }
  
    function dragStart(e) {
      e.preventDefault();
  
      pmouseX = e.clientX;
  
      dom.addClass(gui.__closeButton, GUI.CLASS_DRAG);
      dom.bind(window, 'mousemove', drag);
      dom.bind(window, 'mouseup', dragStop);
  
      return false;
    }
  
    dom.bind(gui.__resize_handle, 'mousedown', dragStart);
    dom.bind(gui.__closeButton, 'mousedown', dragStart);
  
    gui.domElement.insertBefore(gui.__resize_handle, gui.domElement.firstElementChild);
  }
  
  function setWidth(gui, w) {
    gui.domElement.style.width = w + 'px';
    // Auto placed save-rows are position fixed, so we have to
    // set the width manually if we want it to bleed to the edge
    if (gui.__save_row && gui.autoPlace) {
      gui.__save_row.style.width = w + 'px';
    }
    if (gui.__closeButton) {
      gui.__closeButton.style.width = w + 'px';
    }
  }
  
  function getCurrentPreset(gui, useInitialValues) {
    const toReturn = {};
  
    // For each object I'm remembering
    common.each(gui.__rememberedObjects, function(val, index) {
      const savedValues = {};
  
      // The controllers I've made for thcommon.isObject by property
      const controllerMap =
        gui.__rememberedObjectIndecesToControllers[index];
  
      // Remember each value for each property
      common.each(controllerMap, function(controller, property) {
        savedValues[property] = useInitialValues ? controller.initialValue : controller.getValue();
      });
  
      // Save the values for thcommon.isObject
      toReturn[index] = savedValues;
    });
  
    return toReturn;
  }
  
  function setPresetSelectIndex(gui) {
    for (let index = 0; index < gui.__preset_select.length; index++) {
      if (gui.__preset_select[index].value === gui.preset) {
        gui.__preset_select.selectedIndex = index;
      }
    }
  }
  
  function updateDisplays(controllerArray) {
    if (controllerArray.length !== 0) {
      requestAnimationFrame.call(window, function() {
        updateDisplays(controllerArray);
      });
    }
  
    common.each(controllerArray, function(c) {
      c.updateDisplay();
    });
  }
  
  export default GUI;
  