/**
 * SVG Document
 *
 * @param {number} floor_plan_id The floor_plan_id to show.
 * @param {object} state Shared state.
 */
beestat.component.floor_plan = function(floor_plan_id, state) {
  this.toolbar_buttons_ = {};
  this.floor_plan_id_ = floor_plan_id;

  beestat.component.apply(this, arguments);

  /**
   * Override this component's state with a state common to all floor plan
   * entities.
   */
  this.state_ = state;
};
beestat.extend(beestat.component.floor_plan, beestat.component);

/**
 * Whether current user has early access.
 *
 * @return {boolean}
 */
beestat.component.floor_plan.prototype.has_early_access_ = function() {
  return beestat.user.has_early_access() === true;
};

/**
 * Render the SVG document to the parent.
 *
 * @param {rocket.Elements} parent
 *
 * @return {beestat.component.floor_plan} This
 */
beestat.component.floor_plan.prototype.render = function(parent) {
  const self = this;

  this.parent_ = parent;

  this.svg_ = document.createElementNS('http://www.w3.org/2000/svg', 'svg');

  this.defs_ = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
  this.svg_.appendChild(this.defs_);

  this.g_ = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  this.svg_.appendChild(this.g_);

  this.add_grid_();

  this.width_ = this.state_.floor_plan_width || 800;
  this.height_ = 500;

  this.svg_.setAttribute('width', this.width_);
  this.svg_.setAttribute('height', this.height_);

  this.svg_.style.background = beestat.style.color.bluegray.dark;
  this.svg_.style.userSelect = 'none';
  this.svg_.style.touchAction = 'none';

  this.set_zoomable_();
  this.set_draggable_();

  if (this.state_.floor_plan_view_box === undefined) {
    this.view_box_ = {
      'x': 0,
      'y': 0,
      'width': this.width_,
      'height': this.height_
    };
  } else {
    this.view_box_ = this.state_.floor_plan_view_box;
  }
  this.update_view_box_();

  this.toolbar_container_ = $.createElement('div');
  const toolbar_left = beestat.style.size.gutter;
  const toolbar_column_width = 40;
  const toolbar_column_gap = beestat.style.size.gutter / 2;
  this.toolbar_container_.style({
    'position': 'absolute',
    'top': beestat.style.size.gutter,
    'left': toolbar_left,
    'width': '40px'
  });
  parent.appendChild(this.toolbar_container_);

  this.toolbar_container_secondary_ = $.createElement('div');
  this.toolbar_container_secondary_.style({
    'position': 'absolute',
    'top': beestat.style.size.gutter,
    'left': toolbar_left + toolbar_column_width + toolbar_column_gap,
    'width': '40px'
  });
  parent.appendChild(this.toolbar_container_secondary_);

  if (this.state_.show_layers_sidebar !== true) {
    this.floors_container_ = $.createElement('div');
    this.floors_container_.style({
      'position': 'absolute',
      'top': beestat.style.size.gutter,
      'left': beestat.style.size.gutter * 4
    });
    parent.appendChild(this.floors_container_);
  }

  this.update_toolbar();

  if (this.state_.show_layers_sidebar !== true) {
    this.infobox_container_ = $.createElement('div');
    this.infobox_container_.style({
      'position': 'absolute',
      'color': beestat.style.color.gray.base,
      'top': beestat.style.size.gutter,
      'right': beestat.style.size.gutter,
      'line-height': 32,
      'user-select': 'none'
    });
    parent.appendChild(this.infobox_container_);
  }

  this.update_infobox();

  parent.appendChild(this.svg_);

  this.keydown_handler_ = function(e) {
    if (e.target.nodeName === 'BODY') {
      if (e.key === 'Escape') {
        if (
          self.state_.active_room_entity !== undefined ||
          self.state_.active_surface_entity !== undefined ||
          self.state_.active_tree_entity !== undefined ||
          self.state_.active_opening_entity !== undefined ||
          self.state_.active_light_source_entity !== undefined
        ) {
          self.clear_room_();
        }
      } else if (e.key === 'Delete') {
        if (self.state_.active_point_entity !== undefined) {
          self.remove_point_();
        } else {
          self.remove_active_entity_();
        }
      } else if (e.key.toLowerCase() === 'r') {
        if (e.ctrlKey === false) {
          self.add_room_();
        }
      } else if (e.key.toLowerCase() === 'f') {
        if (e.ctrlKey === false && self.has_early_access_() === true) {
          self.add_surface_();
        }
      } else if (e.key.toLowerCase() === 't') {
        if (e.ctrlKey === false && self.has_early_access_() === true) {
          self.add_tree_();
        }
      } else if (e.key.toLowerCase() === 'o') {
        if (e.ctrlKey === false && self.has_early_access_() === true) {
          self.add_opening_();
        }
      } else if (e.key.toLowerCase() === 'l') {
        if (e.ctrlKey === false && self.has_early_access_() === true) {
          self.add_light_source_();
        }
      } else if (e.key.toLowerCase() === 's') {
        self.toggle_snapping_();
      } else if (
        e.key.toLowerCase() === 'c' &&
        e.ctrlKey === true &&
        self.has_early_access_() === true &&
        self.state_.active_surface_entity !== undefined
      ) {
        self.state_.copied_object = {
          'type': 'surface',
          'data': beestat.clone(self.state_.active_surface_entity.get_surface())
        };
      } else if (
        e.key.toLowerCase() === 'c' &&
        e.ctrlKey === true &&
        self.state_.active_room_entity !== undefined
      ) {
        self.state_.copied_object = {
          'type': 'room',
          'data': beestat.clone(self.state_.active_room_entity.get_room())
        };
      } else if (
        e.key.toLowerCase() === 'c' &&
        e.ctrlKey === true &&
        self.has_early_access_() === true &&
        self.state_.active_tree_entity !== undefined
      ) {
        self.state_.copied_object = {
          'type': 'tree',
          'data': beestat.clone(self.state_.active_tree_entity.get_tree())
        };
      } else if (
        e.key.toLowerCase() === 'c' &&
        e.ctrlKey === true &&
        self.state_.active_opening_entity !== undefined
      ) {
        self.state_.copied_object = {
          'type': 'opening',
          'data': beestat.clone(self.state_.active_opening_entity.get_opening())
        };
      } else if (
        e.key.toLowerCase() === 'c' &&
        e.ctrlKey === true &&
        self.state_.active_light_source_entity !== undefined
      ) {
        self.state_.copied_object = {
          'type': 'light_source',
          'data': beestat.clone(self.state_.active_light_source_entity.get_light_source())
        };
      } else if (
        e.key.toLowerCase() === 'v' &&
        e.ctrlKey === true &&
        self.has_early_access_() === true &&
        self.state_.copied_object !== undefined &&
        self.state_.copied_object.type === 'surface'
      ) {
        self.add_surface_(self.state_.copied_object.data);
      } else if (
        e.key.toLowerCase() === 'v' &&
        e.ctrlKey === true &&
        self.has_early_access_() === true &&
        self.state_.copied_object !== undefined &&
        self.state_.copied_object.type === 'tree'
      ) {
        self.add_tree_(self.state_.copied_object.data);
      } else if (
        e.key.toLowerCase() === 'v' &&
        e.ctrlKey === true &&
        self.state_.copied_object !== undefined &&
        self.state_.copied_object.type === 'room'
      ) {
        self.add_room_(self.state_.copied_object.data);
      } else if (
        e.key.toLowerCase() === 'v' &&
        e.ctrlKey === true &&
        self.state_.copied_object !== undefined &&
        self.state_.copied_object.type === 'opening'
      ) {
        self.add_opening_(self.state_.copied_object.data);
      } else if (
        e.key.toLowerCase() === 'v' &&
        e.ctrlKey === true &&
        self.state_.copied_object !== undefined &&
        self.state_.copied_object.type === 'light_source'
      ) {
        self.add_light_source_(self.state_.copied_object.data);
      } else if (
        e.key.toLowerCase() === 'z' &&
        e.ctrlKey === true
      ) {
        self.undo_();
      } else if (
        e.key.toLowerCase() === 'y' &&
        e.ctrlKey === true
      ) {
        self.redo_();
      } else if (
        e.key === 'ArrowLeft' ||
        e.key === 'ArrowRight' ||
        e.key === 'ArrowUp' ||
        e.key === 'ArrowDown'
      ) {
        const entity =
          self.state_.active_point_entity ||
          self.state_.active_wall_entity ||
          self.state_.active_opening_entity ||
          self.state_.active_light_source_entity ||
          self.state_.active_surface_entity ||
          self.state_.active_room_entity ||
          self.state_.active_tree_entity;

        if (entity !== undefined) {
          let x = entity.get_x();
          let y = entity.get_y();
          if (
            self.state_.active_opening_entity !== undefined &&
            entity === self.state_.active_opening_entity &&
            typeof entity.get_center_ === 'function'
          ) {
            const opening_center = entity.get_center_();
            x = opening_center.x;
            y = opening_center.y;
          }

          switch (e.key) {
          case 'ArrowLeft':
            entity.set_xy(x === null ? null : x - 1, y, 'update');
            break;
          case 'ArrowRight':
            entity.set_xy(x === null ? null : x + 1, y, 'update');
            break;
          case 'ArrowUp':
            entity.set_xy(x, y === null ? null : y - 1, 'update');
            break;
          case 'ArrowDown':
            entity.set_xy(x, y === null ? null : y + 1, 'update');
            break;
          }

          e.preventDefault();
        }
      }
    }
  };

  window.addEventListener('keydown', this.keydown_handler_);

  this.rendered_ = true;

  return this;
};

/**
 * Update the view box with the current values. Cap so the grid doesn't go out
 * of view.
 */
beestat.component.floor_plan.prototype.update_view_box_ = function() {
  // Cap x/y pan
  const min_x = this.grid_pixels_ / -2;
  const max_x = (this.grid_pixels_ / 2) - (this.width_ * this.get_scale());
  this.view_box_.x = Math.min(Math.max(this.view_box_.x, min_x), max_x);

  const min_y = this.grid_pixels_ / -2;
  const max_y = (this.grid_pixels_ / 2) - (this.height_ * this.get_scale());
  this.view_box_.y = Math.min(Math.max(this.view_box_.y, min_y), max_y);

  this.svg_.setAttribute(
    'viewBox',
    this.view_box_.x + ' ' + this.view_box_.y + ' ' + this.view_box_.width + ' ' + this.view_box_.height
  );

  this.state_.floor_plan_view_box = this.view_box_;
};

/**
 * Add a helpful grid.
 */
beestat.component.floor_plan.prototype.add_grid_ = function() {
  const pixels_per_small_grid = 12;
  const small_grids_per_large_grid = 10;
  const pixels_per_large_grid = pixels_per_small_grid * small_grids_per_large_grid;

  const large_grid_repeat = 100;
  this.grid_pixels_ = pixels_per_large_grid * large_grid_repeat;

  const grid_small_pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
  grid_small_pattern.setAttribute('id', 'grid_small');
  grid_small_pattern.setAttribute('width', pixels_per_small_grid);
  grid_small_pattern.setAttribute('height', pixels_per_small_grid);
  grid_small_pattern.setAttribute('patternUnits', 'userSpaceOnUse');
  this.defs_.appendChild(grid_small_pattern);

  const grid_small_path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  grid_small_path.setAttribute('d', 'M ' + pixels_per_small_grid + ' 0 L 0 0 0 ' + pixels_per_small_grid);
  grid_small_path.setAttribute('fill', 'none');
  grid_small_path.setAttribute('stroke', beestat.style.color.bluegreen.dark);
  grid_small_path.setAttribute('stroke-width', '0.5');
  grid_small_pattern.appendChild(grid_small_path);

  const grid_large_pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
  grid_large_pattern.setAttribute('id', 'grid_large');
  grid_large_pattern.setAttribute('width', pixels_per_large_grid);
  grid_large_pattern.setAttribute('height', pixels_per_large_grid);
  grid_large_pattern.setAttribute('patternUnits', 'userSpaceOnUse');
  this.defs_.appendChild(grid_large_pattern);

  const grid_large_rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
  grid_large_rect.setAttribute('width', pixels_per_large_grid);
  grid_large_rect.setAttribute('height', pixels_per_large_grid);
  grid_large_rect.setAttribute('fill', 'url("#grid_small")');
  grid_large_pattern.appendChild(grid_large_rect);

  const grid_large_path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  grid_large_path.setAttribute('d', 'M ' + pixels_per_large_grid + ' 0 L 0 0 0 ' + pixels_per_large_grid);
  grid_large_path.setAttribute('fill', 'none');
  grid_large_path.setAttribute('stroke', beestat.style.color.bluegreen.dark);
  grid_large_path.setAttribute('stroke-width', '1');
  grid_large_pattern.appendChild(grid_large_path);

  const grid_rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
  grid_rect.setAttribute('x', this.grid_pixels_ / -2);
  grid_rect.setAttribute('y', this.grid_pixels_ / -2);
  grid_rect.setAttribute('width', this.grid_pixels_);
  grid_rect.setAttribute('height', this.grid_pixels_);
  grid_rect.setAttribute('fill', 'url("#grid_large")');
  this.g_.appendChild(grid_rect);
};

/**
 * Make the SVG document zoomable.
 */
beestat.component.floor_plan.prototype.set_zoomable_ = function() {
  const self = this;

  this.wheel_handler_ = function(e) {
    if (
      e.ctrlKey === true &&
      self.parent_[0].contains(e.target)
    ) {
      e.preventDefault();

      if (e.wheelDelta < 0) {
        self.zoom_out_(e);
      } else {
        self.zoom_in_(e);
      }
    }
  };

  window.addEventListener('wheel', this.wheel_handler_, {'passive': false});
};

/**
 * Make the SVG document draggabe.
 */
beestat.component.floor_plan.prototype.set_draggable_ = function() {
  const self = this;

  const mousedown_handler = function(e) {
    // Prevent things underneath from also dragging.
    e.stopPropagation();

    self.drag_start_mouse_ = {
      'x': e.clientX || e.touches[0].clientX,
      'y': e.clientY || e.touches[0].clientY
    };

    self.drag_start_pan_ = {
      'x': self.view_box_.x,
      'y': self.view_box_.y
    };

    self.dragging_ = true;
  };

  this.mousemove_handler_ = function(e) {
    if (self.dragging_ === true) {
      const dx = ((e.clientX || e.touches[0].clientX) - self.drag_start_mouse_.x);
      const dy = ((e.clientY || e.touches[0].clientY) - self.drag_start_mouse_.y);
      self.view_box_.x = self.drag_start_pan_.x - (dx * self.get_scale());
      self.view_box_.y = self.drag_start_pan_.y - (dy * self.get_scale());
      self.update_view_box_();
    }
  };

  this.mouseup_handler_ = function(e) {
    // Deselect when clicking on the background.
    if (
      self.parent_.contains(e.target) &&
      self.drag_start_mouse_ !== undefined &&
      e.clientX === self.drag_start_mouse_.x &&
      e.clientY === self.drag_start_mouse_.y
    ) {
      self.clear_room_();
    }

    self.dragging_ = false;
  };

  this.svg_.addEventListener('mousedown', mousedown_handler.bind(this));
  this.svg_.addEventListener('touchstart', mousedown_handler.bind(this));

  window.addEventListener('mousemove', this.mousemove_handler_);
  window.addEventListener('touchmove', this.mousemove_handler_);

  window.addEventListener('mouseup', this.mouseup_handler_);
  window.addEventListener('touchend', this.mouseup_handler_);
};

/**
 * Get the root group so other things can put stuff here.
 *
 * @return {SVGGElement} The root group.
 */
beestat.component.floor_plan.prototype.get_g = function() {
  return this.g_;
};

/**
 * Get the scale.
 *
 * @return {SVGGElement} The scale.
 */
beestat.component.floor_plan.prototype.get_scale = function() {
  return this.view_box_.width / this.width_;
};

/**
 * Get the grid pixels.
 *
 * @return {SVGGElement} The scale.
 */
beestat.component.floor_plan.prototype.get_grid_pixels = function() {
  return this.grid_pixels_;
};

/**
 * Convert from screen (global) coordinates to svg (local) coordinates.
 *
 * @param {Event} e
 *
 * @return {SVGPoint} A point in the SVG local coordinate space.
 */
beestat.component.floor_plan.prototype.get_local_point = function(e) {
  const global_point = this.svg_.createSVGPoint();
  global_point.x = e.clientX;
  global_point.y = e.clientY;

  return global_point.matrixTransform(
    this.svg_.getScreenCTM().inverse()
  );
};

/**
 * Remove this component from the page.
 */
beestat.component.floor_plan.prototype.dispose = function() {
  if (this.rendered_ === true) {
    window.removeEventListener('keydown', this.keydown_handler_);
    window.removeEventListener('wheel', this.wheel_handler_);
    window.removeEventListener('mousemove', this.mousemove_handler_);
    window.removeEventListener('touchmove', this.mousemove_handler_);
    window.removeEventListener('mouseup', this.mouseup_handler_);
    window.removeEventListener('touchend', this.mouseup_handler_);
    this.rendered_ = false;
  }
};

/**
 * Update the toolbar to match the current state.
 */
beestat.component.floor_plan.prototype.update_toolbar = function() {
  const self = this;
  const tree_group = this.get_tree_group_();

  if (this.tile_group_ !== undefined) {
    this.tile_group_.dispose();
  }

  if (this.tile_group_floors_ !== undefined) {
    this.tile_group_floors_.dispose();
  }
  if (this.tile_group_secondary_ !== undefined) {
    this.tile_group_secondary_.dispose();
  }

  this.tile_group_ = new beestat.component.tile_group();
  this.tile_group_secondary_ = new beestat.component.tile_group();

  // Add room
  this.tile_group_.add_tile(new beestat.component.tile()
    .set_icon('view_quilt')
    .set_title('Add Room [R]')
    .set_text_color(beestat.style.color.gray.light)
    .set_background_color(beestat.style.color.bluegray.base)
    .set_background_hover_color(beestat.style.color.bluegray.light)
    .addEventListener('click', function() {
      self.add_room_();
    })
  );

  if (this.has_early_access_() === true) {
    // Add opening
    this.tile_group_.add_tile(new beestat.component.tile()
      .set_icon('window_closed_variant')
      .set_title('Add Opening [O]')
      .set_text_color(beestat.style.color.gray.light)
      .set_background_color(beestat.style.color.bluegray.base)
      .set_background_hover_color(beestat.style.color.bluegray.light)
      .addEventListener('click', function() {
        self.add_opening_();
      })
    );

    // Add surface
    this.tile_group_.add_tile(new beestat.component.tile()
      .set_icon('texture_box')
      .set_title('Add Surface [F]')
      .set_text_color(beestat.style.color.gray.light)
      .set_background_color(beestat.style.color.bluegray.base)
      .set_background_hover_color(beestat.style.color.bluegray.light)
      .addEventListener('click', function() {
        self.add_surface_();
      })
    );

    // Add window (placeholder - no action yet)
    // this.tile_group_.add_tile(new beestat.component.tile()
    //   .set_icon('window_closed_variant')
    //   .set_title('Add Window')
    //   .set_text_color(beestat.style.color.gray.light)
    //   .set_background_color(beestat.style.color.bluegray.base)
    //   .set_background_hover_color(beestat.style.color.bluegray.light)
    // );

    // Add tree (first floor only)
    const add_tree_button = new beestat.component.tile()
      .set_icon('tree')
      .set_title('Add Tree [T]')
      .set_background_color(beestat.style.color.bluegray.base);
    this.tile_group_.add_tile(add_tree_button);

    if (this.state_.active_group === tree_group) {
      add_tree_button
        .set_background_hover_color(beestat.style.color.bluegray.light)
        .set_text_color(beestat.style.color.gray.light)
        .addEventListener('click', this.add_tree_.bind(this));
    } else {
      add_tree_button
        .set_text_color(beestat.style.color.bluegray.dark);
    }

    // Add light source
    this.tile_group_.add_tile(new beestat.component.tile()
      .set_icon('lightbulb_on')
      .set_title('Add Light Source [L]')
      .set_text_color(beestat.style.color.gray.light)
      .set_background_color(beestat.style.color.bluegray.base)
      .set_background_hover_color(beestat.style.color.bluegray.light)
      .addEventListener('click', function() {
        self.add_light_source_();
      })
    );
  }

  // Remove selected room, opening, surface, or tree
  const remove_button = new beestat.component.tile()
    .set_icon('card_remove_outline')
    .set_title('Remove [Delete]')
    .set_background_color(beestat.style.color.bluegray.base);
  this.tile_group_.add_tile(remove_button);

  if (
    this.state_.active_room_entity !== undefined ||
    this.state_.active_opening_entity !== undefined ||
    this.state_.active_surface_entity !== undefined ||
    this.state_.active_tree_entity !== undefined ||
    this.state_.active_light_source_entity !== undefined
  ) {
    remove_button
      .set_background_hover_color(beestat.style.color.bluegray.light)
      .set_text_color(beestat.style.color.red.base)
      .addEventListener('click', this.remove_active_entity_.bind(this));
  } else {
    remove_button
      .set_text_color(beestat.style.color.bluegray.dark);
  }

  // Add point
  const add_point_button = new beestat.component.tile()
    .set_icon('vector_square_plus')
    .set_title('Add Point [Double click]')
    .set_background_color(beestat.style.color.bluegray.base);
  this.tile_group_.add_tile(add_point_button);

  if (this.state_.active_wall_entity !== undefined) {
    add_point_button
      .set_background_hover_color(beestat.style.color.bluegray.light)
      .set_text_color(beestat.style.color.gray.light)
      .addEventListener('click', this.add_point_.bind(this));
  } else {
    add_point_button
      .set_text_color(beestat.style.color.bluegray.dark);
  }

  // Remove point
  const remove_point_button = new beestat.component.tile()
    .set_background_color(beestat.style.color.bluegray.base)
    .set_title('Remove Point [Delete]')
    .set_icon('vector_square_remove');
  this.tile_group_.add_tile(remove_point_button);

  if (
    this.state_.active_point_entity !== undefined &&
    this.get_active_shape_entity_() !== undefined &&
    this.get_active_shape_entity_().get_room().points.length > 3
  ) {
    remove_point_button
      .set_background_hover_color(beestat.style.color.bluegray.light)
      .set_text_color(beestat.style.color.red.base)
      .addEventListener('click', this.remove_point_.bind(this));
  } else {
    remove_point_button
      .set_text_color(beestat.style.color.bluegray.dark);
  }

  // Toggle snap to grid
  let snapping_icon;
  let snapping_title;
  if (this.state_.snapping === true) {
    snapping_icon = 'grid';
    snapping_title = 'Disable Snapping [S]';
  } else {
    snapping_icon = 'grid_off';
    snapping_title = 'Enable Snapping [S]';
  }

  this.tile_group_.add_tile(new beestat.component.tile()
    .set_icon(snapping_icon)
    .set_title(snapping_title)
    .set_text_color(beestat.style.color.gray.light)
    .set_background_color(beestat.style.color.bluegray.base)
    .set_background_hover_color(beestat.style.color.bluegray.light)
    .addEventListener('click', this.toggle_snapping_.bind(this))
  );

  // Undo
  const undo_button = new beestat.component.tile()
    .set_icon('undo')
    .set_title('Undo [Ctrl+Z]')
    .set_background_color(beestat.style.color.bluegray.base);
  this.tile_group_secondary_.add_tile(undo_button);

  if (
    this.can_undo_() === true
  ) {
    undo_button
      .set_background_hover_color(beestat.style.color.bluegray.light)
      .set_text_color(beestat.style.color.gray.light)
      .addEventListener('click', function() {
        self.undo_();
      });
  } else {
    undo_button
      .set_text_color(beestat.style.color.bluegray.dark);
  }

  // Redo
  const redo_button = new beestat.component.tile()
    .set_icon('redo')
    .set_title('Redo [Ctrl+Y]')
    .set_background_color(beestat.style.color.bluegray.base);
  this.tile_group_secondary_.add_tile(redo_button);

  if (
    this.can_redo_() === true
  ) {
    redo_button
      .set_background_hover_color(beestat.style.color.bluegray.light)
      .set_text_color(beestat.style.color.gray.light)
      .addEventListener('click', function() {
        self.redo_();
      });
  } else {
    redo_button
      .set_text_color(beestat.style.color.bluegray.dark);
  }

  // Zoom in
  const zoom_in_button = new beestat.component.tile()
    .set_icon('magnify_plus_outline')
    .set_title('Zoom In')
    .set_background_color(beestat.style.color.bluegray.base);
  this.tile_group_secondary_.add_tile(zoom_in_button);

  if (
    this.can_zoom_in_() === true
  ) {
    zoom_in_button
      .set_background_hover_color(beestat.style.color.bluegray.light)
      .set_text_color(beestat.style.color.gray.light)
      .addEventListener('click', function() {
        self.zoom_in_();
      });
  } else {
    zoom_in_button
      .set_text_color(beestat.style.color.bluegray.dark);
  }

  // Zoom out
  const zoom_out_button = new beestat.component.tile()
    .set_icon('magnify_minus_outline')
    .set_title('Zoom out')
    .set_background_color(beestat.style.color.bluegray.base);
  this.tile_group_secondary_.add_tile(zoom_out_button);

  if (
    this.can_zoom_out_() === true
  ) {
    zoom_out_button
      .set_background_hover_color(beestat.style.color.bluegray.light)
      .set_text_color(beestat.style.color.gray.light)
      .addEventListener('click', function() {
        self.zoom_out_();
      });
  } else {
    zoom_out_button
      .set_text_color(beestat.style.color.bluegray.dark);
  }

  // Render
  this.tile_group_.render(this.toolbar_container_);
  this.tile_group_secondary_.render(this.toolbar_container_secondary_);

  // FLOORS
  if (this.floors_container_ !== undefined) {
    this.tile_group_floors_ = new beestat.component.tile_group();

    const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];

    const sorted_groups = Object.values(floor_plan.data.groups)
      .sort(function(a, b) {
        return (a.elevation || 0) - (b.elevation || 0);
      });

    let icon_number = 1;
    sorted_groups.forEach(function(group) {
      const button = new beestat.component.tile()
        .set_title(group.name)
        .set_shadow(false)
        .set_text_hover_color(beestat.style.color.lightblue.light)
        .set_text_color(beestat.style.color.lightblue.base);

      let icon;
      if (group.elevation < 0) {
        icon = 'alpha_b';
      } else {
        icon = 'numeric_' + icon_number++;
      }

      if (group === self.state_.active_group) {
        button
          .set_icon(icon + '_box');
      } else {
        button
          .set_icon(icon)
          .addEventListener('click', function() {
            self.set_active_group(group);
          });
      }

      self.tile_group_floors_.add_tile(button);
    });

    this.tile_group_floors_.render(this.floors_container_);
  }
};

/**
 * Update the infobox to match the current state.
 */
beestat.component.floor_plan.prototype.update_infobox = function() {
  if (this.infobox_container_ === undefined) {
    return;
  }

  const parts = [];
  if (this.state_.active_tree_entity !== undefined) {
    const tree = this.state_.active_tree_entity.get_tree();
    parts.push('Tree');
    parts.push(
      beestat.distance({
        'distance': tree.diameter || 0,
        'units': true,
        'round': 0
      }) + ' dia'
    );
  } else if (this.state_.active_surface_entity !== undefined) {
    parts.push('Surface');
    parts.push(
      beestat.area({
        'input_area_unit': 'in²',
        'area': beestat.floor_plan.get_area_room(this.state_.active_surface_entity.get_surface()),
        'round': 0,
        'units': true
      })
    );
  } else if (this.state_.active_room_entity !== undefined) {
    parts.push(this.state_.active_room_entity.get_room().name || 'Unnamed Room');
    parts.push(
      beestat.area({
        'input_area_unit': 'in²',
        'area': beestat.floor_plan.get_area_room(this.state_.active_room_entity.get_room()),
        'round': 0,
        'units': true
      })
    );
  } else if (this.state_.active_opening_entity !== undefined) {
    const opening = this.state_.active_opening_entity.get_opening();
    const opening_width = this.get_opening_width_(opening);
    parts.push('Opening');
    parts.push((opening.type || 'empty').toUpperCase());
    parts.push(
      beestat.distance({
        'distance': opening_width,
        'units': true,
        'round': 0
      }) + ' w'
    );
  } else if (this.state_.active_light_source_entity !== undefined) {
    parts.push('Light Source');
  } else {
    parts.push(this.state_.active_group.name || 'Unnamed Floor');
    parts.push(
      beestat.area({
        'input_area_unit': 'in²',
        'area': beestat.floor_plan.get_area_group(this.state_.active_group),
        'round': 0,
        'units': true
      })
    );
  }
  this.infobox_container_.innerText(parts.join(' • '));
};

/**
 * Toggle snapping.
 */
beestat.component.floor_plan.prototype.toggle_snapping_ = function() {
  this.state_.snapping = !this.state_.snapping;
  this.update_toolbar();
};

/**
 * Add a new surface.
 *
 * @param {object} surface Optional surface to copy from.
 */
beestat.component.floor_plan.prototype.add_surface_ = function(surface) {
  if (this.has_early_access_() !== true) {
    return;
  }

  const default_surface_color = '#9a9a96';
  this.save_buffer();

  if (this.state_.active_group.surfaces === undefined) {
    this.state_.active_group.surfaces = [];
  }

  const svg_view_box = this.view_box_;

  let new_surface;
  if (surface === undefined) {
    const new_surface_size = 120;
    new_surface = {
      'surface_id': window.crypto.randomUUID(),
      'x': svg_view_box.x + (svg_view_box.width / 2) - (new_surface_size / 2),
      'y': svg_view_box.y + (svg_view_box.height / 2) - (new_surface_size / 2),
      'color': default_surface_color,
      'height': 0,
      'editor_hidden': false,
      'editor_locked': false,
      'points': [
        {
          'x': 0,
          'y': 0
        },
        {
          'x': new_surface_size,
          'y': 0
        },
        {
          'x': new_surface_size,
          'y': new_surface_size
        },
        {
          'x': 0,
          'y': new_surface_size
        }
      ]
    };
  } else {
    let min_x = Infinity;
    let max_x = -Infinity;
    let min_y = Infinity;
    let max_y = -Infinity;

    surface.points.forEach(function(point) {
      min_x = Math.min(surface.x + point.x, min_x);
      max_x = Math.max(surface.x + point.x, max_x);
      min_y = Math.min(surface.y + point.y, min_y);
      max_y = Math.max(surface.y + point.y, max_y);
    });

    new_surface = {
      'surface_id': window.crypto.randomUUID(),
      'x': svg_view_box.x + (svg_view_box.width / 2) - ((max_x - min_x) / 2),
      'y': svg_view_box.y + (svg_view_box.height / 2) - ((max_y - min_y) / 2),
      'color': surface.color || default_surface_color,
      'height': surface.height || 0,
      'editor_hidden': false,
      'editor_locked': false,
      'points': beestat.clone(surface.points)
    };
  }

  this.state_.active_group.surfaces.unshift(new_surface);
  new beestat.component.floor_plan_entity.surface(this, this.state_)
    .set_surface(new_surface)
    .set_group(this.state_.active_group)
    .set_active(true);

  this.dispatchEvent('add_surface');
};

/**
 * Add a new room.
 *
 * @param {object} room Optional room to copy from.
 */
beestat.component.floor_plan.prototype.add_room_ = function(room) {
  this.save_buffer();

  const svg_view_box = this.view_box_;

  let new_room;
  if (room === undefined) {
    const new_room_size = 120;
    new_room = {
      'room_id': window.crypto.randomUUID(),
      'x': svg_view_box.x + (svg_view_box.width / 2) - (new_room_size / 2),
      'y': svg_view_box.y + (svg_view_box.height / 2) - (new_room_size / 2),
      'editor_hidden': false,
      'editor_locked': false,
      'points': [
        {
          'x': 0,
          'y': 0
        },
        {
          'x': new_room_size,
          'y': 0
        },
        {
          'x': new_room_size,
          'y': new_room_size
        },
        {
          'x': 0,
          'y': new_room_size
        }
      ]
    };
  } else {
    let min_x = Infinity;
    let max_x = -Infinity;
    let min_y = Infinity;
    let max_y = -Infinity;

    room.points.forEach(function(point) {
      min_x = Math.min(room.x + point.x, min_x);
      max_x = Math.max(room.x + point.x, max_x);
      min_y = Math.min(room.y + point.y, min_y);
      max_y = Math.max(room.y + point.y, max_y);
    });

    new_room = {
      'room_id': window.crypto.randomUUID(),
      'x': svg_view_box.x + (svg_view_box.width / 2) - ((max_x - min_x) / 2),
      'y': svg_view_box.y + (svg_view_box.height / 2) - ((max_y - min_y) / 2),
      'editor_hidden': false,
      'editor_locked': false,
      'points': beestat.clone(room.points)
    };
  }

  this.state_.active_group.rooms.unshift(new_room);
  new beestat.component.floor_plan_entity.room(this, this.state_)
    .set_room(new_room)
    .set_group(this.state_.active_group)
    .set_active(true);

  this.dispatchEvent('add_room');
};

/**
 * Remove the currently active room.
 */
beestat.component.floor_plan.prototype.remove_room_ = function() {
  this.save_buffer();

  const old_sensor_ids = Object.keys(beestat.floor_plan.get_sensor_ids_map(
    beestat.setting('visualize.floor_plan_id')
  ));

  const self = this;

  const index = this.state_.active_group.rooms.findIndex(function(room) {
    return room === self.state_.active_room_entity.get_room();
  });

  if (this.state_.active_room_entity !== undefined) {
    this.state_.active_room_entity.set_active(false);
  }
  if (this.state_.active_wall_entity !== undefined) {
    this.state_.active_wall_entity.set_active(false);
  }
  if (this.state_.active_point_entity !== undefined) {
    this.state_.active_point_entity.set_active(false);
  }

  this.state_.active_group.rooms.splice(index, 1);

  const new_sensor_ids = Object.keys(beestat.floor_plan.get_sensor_ids_map(
    beestat.setting('visualize.floor_plan_id')
  ));

  // Delete data if the overall sensor set changes so it's re-fetched.
  if (old_sensor_ids.sort().join(' ') !== new_sensor_ids.sort().join(' ')) {
    beestat.cache.delete('data.three_d__runtime_sensor');
  }

  this.dispatchEvent('remove_room');
};

/**
 * Remove the currently active selectable entity (surface, room, opening, or tree).
 */
beestat.component.floor_plan.prototype.remove_active_entity_ = function() {
  if (this.state_.active_light_source_entity !== undefined) {
    this.remove_light_source_();
    return;
  }

  if (this.state_.active_opening_entity !== undefined) {
    this.remove_opening_();
    return;
  }

  if (this.state_.active_surface_entity !== undefined) {
    this.remove_surface_();
    return;
  }

  if (this.state_.active_room_entity !== undefined) {
    this.remove_room_();
    return;
  }

  if (this.state_.active_tree_entity !== undefined) {
    this.remove_tree_();
  }
};

/**
 * Set the active group and clear active entities.
 *
 * @param {object} group
 *
 * @return {beestat.component.floor_plan} This.
 */
beestat.component.floor_plan.prototype.set_active_group = function(group) {
  if (group === undefined || this.state_.active_group === group) {
    return this;
  }

  if (this.state_.active_room_entity !== undefined) {
    this.state_.active_room_entity.set_active(false);
  }
  if (this.state_.active_wall_entity !== undefined) {
    this.state_.active_wall_entity.set_active(false);
  }
  if (this.state_.active_point_entity !== undefined) {
    this.state_.active_point_entity.set_active(false);
  }
  if (this.state_.active_surface_entity !== undefined) {
    this.state_.active_surface_entity.set_active(false);
  }
  if (this.state_.active_tree_entity !== undefined) {
    this.state_.active_tree_entity.set_active(false);
  }
  if (this.state_.active_opening_entity !== undefined) {
    this.state_.active_opening_entity.set_active(false);
  }
  if (this.state_.active_light_source_entity !== undefined) {
    this.state_.active_light_source_entity.set_active(false);
  }

  this.state_.active_group = group;
  this.dispatchEvent('change_group');

  return this;
};

/**
 * Remove the currently active surface.
 */
beestat.component.floor_plan.prototype.remove_surface_ = function() {
  this.save_buffer();

  if (
    this.state_.active_surface_entity === undefined ||
    this.state_.active_group.surfaces === undefined
  ) {
    return;
  }

  const self = this;

  const index = this.state_.active_group.surfaces.findIndex(function(surface) {
    return surface === self.state_.active_surface_entity.get_surface();
  });

  if (index === -1) {
    return;
  }

  this.state_.active_surface_entity.set_active(false);
  if (this.state_.active_wall_entity !== undefined) {
    this.state_.active_wall_entity.set_active(false);
  }
  if (this.state_.active_point_entity !== undefined) {
    this.state_.active_point_entity.set_active(false);
  }

  this.state_.active_group.surfaces.splice(index, 1);

  this.dispatchEvent('remove_surface');
};

/**
 * Add a new opening.
 *
 * @param {object} opening Optional opening to copy from.
 */
beestat.component.floor_plan.prototype.add_opening_ = function(opening) {
  if (this.has_early_access_() !== true) {
    return;
  }

  this.save_buffer();

  if (this.state_.active_group.openings === undefined) {
    this.state_.active_group.openings = [];
  }

  const svg_view_box = this.view_box_;
  const requested_opening_type = (opening || {}).type;
  const opening_type = ['empty', 'door', 'window', 'glass'].includes(requested_opening_type)
    ? requested_opening_type
    : 'empty';

  let default_width = 36;
  let default_height = 78;
  let default_elevation = 0;
  let default_color = '#7a573b';
  if (opening_type === 'window' || opening_type === 'glass') {
    default_width = 48;
    default_height = 60;
    default_elevation = 24;
  }

  const width = Math.max(12, Number((opening || {}).width || default_width));
  const height = Math.max(1, Number((opening || {}).height || default_height));
  const elevation = Number((opening || {}).elevation !== undefined ? opening.elevation : default_elevation);
  const center_x = Number((opening || {}).x || (svg_view_box.x + (svg_view_box.width / 2)));
  const center_y = Number((opening || {}).y || (svg_view_box.y + (svg_view_box.height / 2)));
  const half_width = width / 2;

  const new_opening = {
    'opening_id': window.crypto.randomUUID(),
    'x': center_x,
    'y': center_y,
    'height': height,
    'elevation': elevation,
    'points': (opening || {}).points && (opening || {}).points.length === 2
      ? beestat.clone(opening.points)
      : [
        {
          'x': center_x - half_width,
          'y': center_y
        },
        {
          'x': center_x + half_width,
          'y': center_y
        }
      ],
    'type': opening_type,
    'color': opening_type === 'door' ? String((opening || {}).color || default_color) : undefined,
    'name': (opening || {}).name,
    'editor_hidden': false,
    'editor_locked': false
  };

  this.state_.active_group.openings.unshift(new_opening);
  new beestat.component.floor_plan_entity.opening(this, this.state_)
    .set_opening(new_opening)
    .set_group(this.state_.active_group)
    .set_active(true);

  this.dispatchEvent('add_opening');
};

/**
 * Get opening width in floor-plan units from points.
 *
 * @param {object} opening
 *
 * @return {number}
 */
beestat.component.floor_plan.prototype.get_opening_width_ = function(opening) {
  if (
    opening !== undefined &&
    opening.points !== undefined &&
    opening.points.length === 2
  ) {
    const dx = Number(opening.points[1].x || 0) - Number(opening.points[0].x || 0);
    const dy = Number(opening.points[1].y || 0) - Number(opening.points[0].y || 0);
    return Math.max(0, Math.sqrt((dx * dx) + (dy * dy)));
  }

  return Math.max(0, Number(opening.width || 0));
};

/**
 * Remove the currently active opening.
 */
beestat.component.floor_plan.prototype.remove_opening_ = function() {
  this.save_buffer();

  if (
    this.state_.active_opening_entity === undefined ||
    this.state_.active_group.openings === undefined
  ) {
    return;
  }

  const self = this;
  const index = this.state_.active_group.openings.findIndex(function(opening) {
    return opening === self.state_.active_opening_entity.get_opening();
  });

  if (index === -1) {
    return;
  }

  this.state_.active_opening_entity.set_active(false);
  this.state_.active_group.openings.splice(index, 1);

  this.dispatchEvent('remove_opening');
};

/**
 * Add a new light source to the active floor.
 *
 * @param {object} light_source Optional light source to copy from.
 */
beestat.component.floor_plan.prototype.add_light_source_ = function(light_source) {
  if (this.has_early_access_() !== true) {
    return;
  }

  this.save_buffer();

  if (this.state_.active_group.light_sources === undefined) {
    this.state_.active_group.light_sources = [];
  }

  const svg_view_box = this.view_box_;
  const new_light_source = {
    'light_source_id': window.crypto.randomUUID(),
    'x': Number((light_source || {}).x || (svg_view_box.x + (svg_view_box.width / 2))),
    'y': Number((light_source || {}).y || (svg_view_box.y + (svg_view_box.height / 2))),
    'elevation': Number((light_source || {}).elevation !== undefined ? light_source.elevation : 72),
    'intensity': ['dim', 'normal', 'bright'].includes((light_source || {}).intensity)
      ? light_source.intensity
      : 'normal',
    'temperature_k': Math.max(1000, Math.min(12000, Math.round(Number((light_source || {}).temperature_k || 4000)))),
    'name': (light_source || {}).name,
    'editor_hidden': false,
    'editor_locked': false
  };

  this.state_.active_group.light_sources.unshift(new_light_source);
  new beestat.component.floor_plan_entity.light_source(this, this.state_)
    .set_light_source(new_light_source)
    .set_group(this.state_.active_group)
    .set_active(true);

  this.dispatchEvent('add_light_source');
};

/**
 * Remove the currently active light source.
 */
beestat.component.floor_plan.prototype.remove_light_source_ = function() {
  this.save_buffer();

  if (
    this.state_.active_light_source_entity === undefined ||
    this.state_.active_group.light_sources === undefined
  ) {
    return;
  }

  const self = this;
  const index = this.state_.active_group.light_sources.findIndex(function(light_source) {
    return light_source === self.state_.active_light_source_entity.get_light_source();
  });

  if (index === -1) {
    return;
  }

  this.state_.active_light_source_entity.set_active(false);
  this.state_.active_group.light_sources.splice(index, 1);

  this.dispatchEvent('remove_light_source');
};

/**
 * Add a new tree to the first floor.
 *
 * @param {object} tree Optional tree to copy from.
 */
beestat.component.floor_plan.prototype.add_tree_ = function(tree) {
  if (this.has_early_access_() !== true) {
    return;
  }

  const tree_group = this.get_tree_group_();
  if (tree_group === undefined || this.state_.active_group !== tree_group) {
    return;
  }

  this.save_buffer();

  if (tree_group.trees === undefined) {
    tree_group.trees = [];
  }

  const svg_view_box = this.view_box_;
  let new_tree;
  if (tree === undefined) {
    new_tree = {
      'tree_id': window.crypto.randomUUID(),
      'x': svg_view_box.x + (svg_view_box.width / 2),
      'y': svg_view_box.y + (svg_view_box.height / 2),
      'height': 120,
      'diameter': 72,
      'type': 'round',
      'editor_hidden': false,
      'editor_locked': false
    };
  } else {
    const tree_type = ['conical', 'round', 'oval'].includes(tree.type)
      ? tree.type
      : 'round';
    new_tree = {
      'tree_id': window.crypto.randomUUID(),
      'x': svg_view_box.x + (svg_view_box.width / 2),
      'y': svg_view_box.y + (svg_view_box.height / 2),
      'height': tree.height || 120,
      'diameter': tree.diameter || 72,
      'type': tree_type,
      'editor_hidden': false,
      'editor_locked': false
    };
  }

  tree_group.trees.unshift(new_tree);
  new beestat.component.floor_plan_entity.tree(this, this.state_)
    .set_tree(new_tree)
    .set_group(tree_group)
    .set_active(true);

  this.dispatchEvent('add_tree');
};

/**
 * Remove the currently active tree.
 */
beestat.component.floor_plan.prototype.remove_tree_ = function() {
  const tree_group = this.get_tree_group_();
  if (
    this.state_.active_tree_entity === undefined ||
    tree_group === undefined ||
    tree_group.trees === undefined
  ) {
    return;
  }

  this.save_buffer();

  const self = this;
  const index = tree_group.trees.findIndex(function(tree) {
    return tree === self.state_.active_tree_entity.get_tree();
  });

  if (index === -1) {
    return;
  }

  this.state_.active_tree_entity.set_active(false);
  tree_group.trees.splice(index, 1);

  this.dispatchEvent('remove_tree');
};

/**
 * Clear the currently active room.
 */
beestat.component.floor_plan.prototype.clear_room_ = function() {
  if (this.state_.active_tree_entity !== undefined) {
    this.state_.active_tree_entity.set_active(false);
  }
  if (this.state_.active_surface_entity !== undefined) {
    this.state_.active_surface_entity.set_active(false);
  }
  if (this.state_.active_room_entity !== undefined) {
    this.state_.active_room_entity.set_active(false);
  }
  if (this.state_.active_wall_entity !== undefined) {
    this.state_.active_wall_entity.set_active(false);
  }
  if (this.state_.active_point_entity !== undefined) {
    this.state_.active_point_entity.set_active(false);
  }
  if (this.state_.active_opening_entity !== undefined) {
    this.state_.active_opening_entity.set_active(false);
  }
  if (this.state_.active_light_source_entity !== undefined) {
    this.state_.active_light_source_entity.set_active(false);
  }
};

/**
 * Remove the currently active point.
 */
beestat.component.floor_plan.prototype.remove_point_ = function() {
  this.save_buffer();

  const active_shape_entity = this.get_active_shape_entity_();
  if (active_shape_entity === undefined) {
    return;
  }

  if (active_shape_entity.get_room().points.length > 3) {
    for (let i = 0; i < active_shape_entity.get_room().points.length; i++) {
      if (this.state_.active_point_entity.get_point() === active_shape_entity.get_room().points[i]) {
        active_shape_entity.get_room().points.splice(i, 1);
        if (this.state_.active_point_entity !== undefined) {
          this.state_.active_point_entity.set_active(false);
        }
        this.dispatchEvent('remove_point');
        break;
      }
    }
  }
};

/**
 * Add a new point to the active wall.
 */
beestat.component.floor_plan.prototype.add_point_ = function() {
  this.state_.active_wall_entity.add_point();
};

/**
 * Get the currently active shape (surface or room).
 *
 * @return {object|undefined}
 */
beestat.component.floor_plan.prototype.get_active_shape_entity_ = function() {
  return this.state_.active_surface_entity || this.state_.active_room_entity;
};

/**
 * Set the width of this component. Also updates the view box to the
 * appropriate values according to the current zoom.
 *
 * @param {number} width
 */
beestat.component.floor_plan.prototype.set_width = function(width) {
  this.view_box_.width = this.view_box_.width * width / this.width_;
  this.width_ = width;
  this.svg_.setAttribute('width', width);
  this.update_view_box_();
  this.state_.floor_plan_width = width;
};

/**
 * Zoom
 *
 * @param {number} scale_delta
 * @param {Event} e
 */
beestat.component.floor_plan.prototype.zoom_ = function(scale_delta, e) {
  let local_point;
  if (e === undefined) {
    local_point = {
      'x': this.width_ / 2,
      'y': this.height_ / 2
    };
  } else {
    local_point = this.get_local_point(e);
  }

  this.view_box_.x -= (local_point.x - this.view_box_.x) * (scale_delta - 1);
  this.view_box_.y -= (local_point.y - this.view_box_.y) * (scale_delta - 1);
  this.view_box_.width *= scale_delta;
  this.view_box_.height *= scale_delta;

  this.update_view_box_();
  this.update_toolbar();

  this.dispatchEvent('zoom');
};

/**
 * Reset the view box
 */
beestat.component.floor_plan.prototype.reset_view_box_ = function() {
  this.view_box_ = {
    'x': 0,
    'y': 0,
    'width': this.width_,
    'height': this.height_
  };
  this.update_view_box_();
};

/**
 * Zoom in
 *
 * @param {Event} e Optional event when zooming to a specific mouse position.
 */
beestat.component.floor_plan.prototype.zoom_in_ = function(e) {
  if (this.can_zoom_in_() === true) {
    this.zoom_(0.9, e);
  }
};

/**
 * Zoom out
 *
 * @param {Event} e Optional event when zooming to a specific mouse position.
 */
beestat.component.floor_plan.prototype.zoom_out_ = function(e) {
  if (this.can_zoom_out_() === true) {
    this.zoom_(1.1, e);
  }
};

/**
 * Whether or not you can zoom in.
 *
 * @return {boolean} Whether or not you can zoom in.
 */
beestat.component.floor_plan.prototype.can_zoom_in_ = function() {
  const min_width = this.width_ / 4;
  const min_height = this.height_ / 4;

  if (
    this.view_box_.width * 0.9 < min_width ||
    this.view_box_.height * 0.9 < min_height
  ) {
    return false;
  }

  return true;
};

/**
 * Whether or not you can zoom out
 *
 * @return {boolean} Whether or not you can zoom in.
 */
beestat.component.floor_plan.prototype.can_zoom_out_ = function() {
  const max_width = this.width_ * 3;
  const max_height = this.height_ * 3;

  if (
    this.view_box_.width * 1.1 > max_width ||
    this.view_box_.height * 1.1 > max_height
  ) {
    return false;
  }

  return true;
};

/**
 * Get the group below the specified one.
 *
 * @param {object} group The current group.
 *
 * @return {object} The group below the current group.
 */
beestat.component.floor_plan.prototype.get_group_below = function(group) {
  let closest_group;
  let closest_elevation_diff = Infinity;

  const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
  floor_plan.data.groups.forEach(function(other_group) {
    if (
      other_group.elevation < group.elevation &&
      group.elevation - other_group.elevation < closest_elevation_diff
    ) {
      closest_elevation_diff = group.elevation - other_group.elevation;
      closest_group = other_group;
    }
  });

  return closest_group;
};

/**
 * Get the first non-basement group (first floor).
 *
 * @return {object|undefined}
 */
beestat.component.floor_plan.prototype.get_tree_group_ = function() {
  const floor_plan = beestat.cache.floor_plan[this.floor_plan_id_];
  const sorted_groups = Object.values(floor_plan.data.groups)
    .sort(function(a, b) {
      return (a.elevation || 0) - (b.elevation || 0);
    });

  const first_floor = sorted_groups.find(function(group) {
    return (group.elevation || 0) >= 0;
  });

  return first_floor || sorted_groups[0];
};

/**
 * Center the view box on the content. Sets zoom and pan.
 */
beestat.component.floor_plan.prototype.center_content = function() {
  const bounding_box = beestat.floor_plan.get_bounding_box(this.floor_plan_id_);

  this.reset_view_box_();
  if (
    bounding_box.x !== Infinity &&
    bounding_box.y !== Infinity
  ) {
    const width = (bounding_box.width) + 50;
    const height = (bounding_box.height) + 50;
    while (
      (
        this.view_box_.width < width ||
        this.view_box_.height < height
      ) &&
      this.can_zoom_out_() === true
    ) {
      this.zoom_out_();
    }

    const center_x = (bounding_box.right + bounding_box.left) / 2;
    const center_y = (bounding_box.bottom + bounding_box.top) / 2;

    this.view_box_.x = center_x - (this.view_box_.width / 2);
    this.view_box_.y = center_y - (this.view_box_.height / 2);

    this.update_view_box_();
  }
};

/**
 * Save the current state to the undo/redo buffer.
 *
 * @param {boolean} clear Whether or not to allow clearing future buffer
 * entries.
 */
beestat.component.floor_plan.prototype.save_buffer = function(clear = true) {
  const buffer_size = 1000;

  if (this.state_.buffer === undefined) {
    this.state_.buffer = [];
    this.state_.buffer_pointer = 0;
  }

  // If the buffer pointer is not at the end, clear those out.
  if (
    clear === true &&
    this.state_.buffer_pointer !== this.state_.buffer.length + 1
  ) {
    this.state_.buffer.length = this.state_.buffer_pointer;
  }

  this.state_.buffer.push({
    'floor_plan': beestat.clone(beestat.cache.floor_plan[beestat.setting('visualize.floor_plan_id')]),
    'active_room_entity': this.state_.active_room_entity,
    'active_surface_entity': this.state_.active_surface_entity,
    'active_tree_entity': this.state_.active_tree_entity,
    'active_opening_entity': this.state_.active_opening_entity,
    'active_light_source_entity': this.state_.active_light_source_entity,
    'active_group_id': this.state_.active_group.group_id
  });

  // If the buffer gets too long shrink it.
  if (this.state_.buffer.length > buffer_size) {
    this.state_.buffer.shift();
  }

  /**
   * Update the buffer pointer. It always points at the index where the next
   * buffer write will happen.
   */
  this.state_.buffer_pointer = this.state_.buffer.length;

  this.update_toolbar();
};

/**
 * Undo
 */
beestat.component.floor_plan.prototype.undo_ = function() {
  if (this.can_undo_() === true) {
    const old_sensor_ids = Object.keys(beestat.floor_plan.get_sensor_ids_map(
      beestat.setting('visualize.floor_plan_id')
    ));

    /**
     * When undoing, first save the buffer if the pointer is at the end to
     * capture the current state then shift the buffer pointer back an extra.
     */
    if (this.state_.buffer_pointer === this.state_.buffer.length) {
      this.save_buffer(false);
      this.state_.buffer_pointer--;
    }

    // Decrement buffer pointer back to the previous row.
    this.state_.buffer_pointer--;

    // Restore the floor plan.
    beestat.cache.floor_plan[this.floor_plan_id_] =
      beestat.clone(this.state_.buffer[this.state_.buffer_pointer].floor_plan);

    // Restore any active room.
    this.state_.active_room_entity =
      this.state_.buffer[this.state_.buffer_pointer].active_room_entity;
    this.state_.active_surface_entity =
      this.state_.buffer[this.state_.buffer_pointer].active_surface_entity;
    this.state_.active_tree_entity =
      this.state_.buffer[this.state_.buffer_pointer].active_tree_entity;
    this.state_.active_opening_entity =
      this.state_.buffer[this.state_.buffer_pointer].active_opening_entity;
    this.state_.active_light_source_entity =
      this.state_.buffer[this.state_.buffer_pointer].active_light_source_entity;

    // Restore any active group.
    this.state_.active_group_id =
      this.state_.buffer[this.state_.buffer_pointer].active_group_id;

    // Delete data if the overall sensor set changes so it's re-fetched.
    const new_sensor_ids = Object.keys(beestat.floor_plan.get_sensor_ids_map(
      beestat.setting('visualize.floor_plan_id')
    ));

    if (old_sensor_ids.sort().join(' ') !== new_sensor_ids.sort().join(' ')) {
      beestat.cache.delete('data.three_d__runtime_sensor');
    }

    this.update_toolbar();
    this.dispatchEvent('undo');
  }
};

/**
 * Whether or not you can undo.
 *
 * @return {boolean}
 */
beestat.component.floor_plan.prototype.can_undo_ = function() {
  return this.state_.buffer_pointer > 0;
};

/**
 * Redo
 */
beestat.component.floor_plan.prototype.redo_ = function() {
  if (this.can_redo_() === true) {
    const old_sensor_ids = Object.keys(beestat.floor_plan.get_sensor_ids_map(
      beestat.setting('visualize.floor_plan_id')
    ));

    this.state_.buffer_pointer++;
    // Restore the floor plan.
    beestat.cache.floor_plan[this.floor_plan_id_] =
      beestat.clone(this.state_.buffer[this.state_.buffer_pointer].floor_plan);

    // Restore any active room.
    this.state_.active_room_entity =
      this.state_.buffer[this.state_.buffer_pointer].active_room_entity;
    this.state_.active_surface_entity =
      this.state_.buffer[this.state_.buffer_pointer].active_surface_entity;
    this.state_.active_tree_entity =
      this.state_.buffer[this.state_.buffer_pointer].active_tree_entity;
    this.state_.active_opening_entity =
      this.state_.buffer[this.state_.buffer_pointer].active_opening_entity;
    this.state_.active_light_source_entity =
      this.state_.buffer[this.state_.buffer_pointer].active_light_source_entity;

    // Restore any active group.
    this.state_.active_group_id =
      this.state_.buffer[this.state_.buffer_pointer].active_group_id;

    // Delete data if the overall sensor set changes so it's re-fetched.
    const new_sensor_ids = Object.keys(beestat.floor_plan.get_sensor_ids_map(
      beestat.setting('visualize.floor_plan_id')
    ));

    if (old_sensor_ids.sort().join(' ') !== new_sensor_ids.sort().join(' ')) {
      beestat.cache.delete('data.three_d__runtime_sensor');
    }

    this.update_toolbar();
    this.dispatchEvent('redo');
  }
};

/**
 * Whether or not you can redo.
 *
 * @return {boolean}
 */
beestat.component.floor_plan.prototype.can_redo_ = function() {
  return this.state_.buffer !== undefined &&
    this.state_.buffer_pointer + 1 < this.state_.buffer.length;
};


