/**
 * Base class for a high level element that exists on the SVG document.
 *
 * @param {object} floor_plan The SVG component this belongs to.
 * @param {object} state Shared state.
 */
beestat.component.floor_plan_entity = function(floor_plan, state) {
  this.floor_plan_ = floor_plan;

  this.x_ = 0;
  this.y_ = 0;

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

  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_entity, beestat.component);

/**
 * Render
 *
 * @param {SVGGElement} parent
 *
 * @return {beestat.component.floor_plan_entity} This.
 */
beestat.component.floor_plan_entity.prototype.render = function(parent) {
  if (this.rendered_ === false) {
    const self = this;

    this.decorate_(this.g_);
    parent.appendChild(this.g_);

    this.apply_transform_();

    // The element should now exist on the DOM.
    window.setTimeout(function() {
      self.dispatchEvent('render');
    }, 0);

    // The render function was called.
    this.rendered_ = true;
  }

  return this;
};

/**
 * Rerender
 *
 * @return {beestat.component.floor_plan_entity} This.
 */
beestat.component.floor_plan_entity.prototype.rerender = function() {
  if (this.rendered_ === true) {
    this.rendered_ = false;

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

    this.decorate_(this.g_);
    old_g.parentNode.replaceChild(this.g_, old_g);

    this.apply_transform_();

    var self = this;
    window.setTimeout(function() {
      self.dispatchEvent('render');
    }, 0);

    this.rendered_ = true;
  }

  return this;
};

/**
 * Bring the current element to the front.
 */
beestat.component.floor_plan_entity.prototype.bring_to_front_ = function() {
  if (this.rendered_ === true) {
    this.g_.parentNode.appendChild(this.g_);
  }
};

/**
 * Make this draggable or not.
 *
 * @param {boolean} draggable Whether or not this is draggable.
 *
 * @return {beestat.component.floor_plan_entity} This.
 */
beestat.component.floor_plan_entity.prototype.set_draggable_ = function(draggable) {
  if (draggable === true) {
    this.g_.addEventListener('mousedown', this.mousedown_handler_.bind(this));
    this.g_.addEventListener('touchstart', this.mousedown_handler_.bind(this));
  }

  this.draggable_ = draggable;

  return this;
};

/**
 * Set the x and y positions of this entity.
 *
 * @param {number} x The x position of this entity.
 * @param {number} y The y position of this entity.
 *
 * @return {beestat.component.floor_plan_entity} This.
 */
beestat.component.floor_plan_entity.prototype.set_xy = function(x, y) {
  if (x !== null) {
    this.x_ = Math.round(x);
  }

  if (y !== null) {
    this.y_ = Math.round(y);
  }

  this.apply_transform_();
  return this;
};

/**
 * Apply all of the relevant transformations to the SVG document.
 *
 * @return {beestat.component.floor_plan_entity} This.
 */
beestat.component.floor_plan_entity.prototype.apply_transform_ = function() {
  const x = this.x_ || 0;
  const y = this.y_ || 0;

  if (x !== 0 || y !== 0) {
    this.g_.setAttribute(
      'transform',
      'translate(' + (this.x_ || 0) + ',' + (this.y_ || 0) + ')'
    );
  } else {
    this.g_.removeAttribute('transform');
  }

  return this;
};
/**
 * Drag start handler
 *
 * @param {Event} e
 */
beestat.component.floor_plan_entity.prototype.mousedown_handler_ = function(e) {
  // Don't propagate to things under me.
  e.stopPropagation();

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

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

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

  this.dragged_ = false;

  this.after_mousedown_handler_(e);
};

/**
 * After mousedown.
 *
 * @param {Event} e
 */
beestat.component.floor_plan_entity.prototype.after_mousedown_handler_ = function(e) {
  // Stub
};

/**
 * This handler gets added after mousedown, so it can be assumed that we want
 * to drag at this point.
 *
 * @param {Event} e
 */
beestat.component.floor_plan_entity.prototype.mousemove_handler_ = function(e) {
  if (this.dragged_ === false) {
    this.dispatchEvent('drag_start');
    this.dragged_ = true;
    this.floor_plan_.save_buffer();
  }

  this.after_mousemove_handler_(e);
};

/**
 * After mousemove.
 *
 * @param {Event} e
 */
beestat.component.floor_plan_entity.prototype.after_mousemove_handler_ = function(e) {
  // Stub
};

/**
 * Drag stop handler
 *
 * @param {Event} e
 */
beestat.component.floor_plan_entity.prototype.mouseup_handler_ = function(e) {
  window.removeEventListener('mousemove', this.mousemove_handler_);
  window.removeEventListener('touchmove', this.mousemove_handler_);
  window.removeEventListener('mouseup', this.mouseup_handler_);
  window.removeEventListener('touchend', this.mouseup_handler_);

  delete this.drag_start_entity_;
  delete this.drag_start_mouse_;

  // If the mouse was actually moved at all then fire the drag stop event.
  if (this.dragged_ === true) {
    this.dispatchEvent('drag_stop');
    this.dispatchEvent('update');
  }

  this.after_mouseup_handler_(e);
};

/**
 * After mouseup.
 *
 * @param {Event} e
 */
beestat.component.floor_plan_entity.prototype.after_mouseup_handler_ = function(e) {
  // Stub
};

/**
 * Get X
 *
 * @return {number} x
 */
beestat.component.floor_plan_entity.prototype.get_x = function() {
  return this.x_;
};

/**
 * Get Y
 *
 * @return {number} y
 */
beestat.component.floor_plan_entity.prototype.get_y = function() {
  return this.y_;
};

/**
 * Collect snap x/y coordinates from selected shape collections.
 *
 * @param {object} options
 * @param {object[]} options.groups
 * @param {Array<{collection: string, point_mode: string}>} options.shape_specs
 * @param {(function(object, object, object): boolean)=} options.should_skip_shape
 *
 * @return {{snap_x: number[], snap_y: number[]}}
 */
beestat.component.floor_plan_entity.prototype.collect_snap_points_ = function(options) {
  const snap_x = {};
  const snap_y = {};
  const groups = Array.isArray(options.groups) ? options.groups : [];
  const shape_specs = Array.isArray(options.shape_specs) ? options.shape_specs : [];
  const should_skip_shape = typeof options.should_skip_shape === 'function'
    ? options.should_skip_shape
    : function() {
      return false;
    };

  groups.forEach(function(group) {
    if (group === undefined || group === null) {
      return;
    }

    shape_specs.forEach(function(shape_spec) {
      const shapes = group[shape_spec.collection];
      if (Array.isArray(shapes) !== true) {
        return;
      }

      shapes.forEach(function(shape) {
        if (shape === undefined || shape.editor_hidden === true) {
          return;
        }
        if (should_skip_shape(shape, shape_spec, group) === true) {
          return;
        }

        if (shape_spec.point_mode === 'point') {
          snap_x[Number(shape.x || 0)] = true;
          snap_y[Number(shape.y || 0)] = true;
          return;
        }

        if (Array.isArray(shape.points) !== true) {
          return;
        }

        shape.points.forEach(function(point) {
          const point_x = Number(point.x || 0);
          const point_y = Number(point.y || 0);
          if (shape_spec.point_mode === 'absolute') {
            snap_x[point_x] = true;
            snap_y[point_y] = true;
            return;
          }

          snap_x[point_x + Number(shape.x || 0)] = true;
          snap_y[point_y + Number(shape.y || 0)] = true;
        });
      });
    });
  });

  return {
    'snap_x': Object.keys(snap_x).map(function(key) {
      return Number(key);
    }),
    'snap_y': Object.keys(snap_y).map(function(key) {
      return Number(key);
    })
  };
};
