module RTanque
  # A `Point` represents an [x, y] coordinate pair in the {RTanque::Arena}
  #
  # ##Usage
  #     @arena = RTanque::Arena.new(100, 100)
  #     # => #<struct RTanque::Arena width=100, height=100>
  #
  #     @point_one = RTanque::Point.new(0, 1, @arena)
  #     # => #<struct RTanque::Point x=0, y=1, arena=#<struct RTanque::Arena width=100, height=100>>
  #
  #     @point_one.on_top_wall?
  #     # => false
  #
  #     @point_one.on_bottom_wall?
  #     # => false
  #
  #     @point_one.on_right_wall?
  #     # => false
  #
  #     @point_one.on_left_wall?
  #     # => true
  #
  #     @point_one.on_wall?
  #     # => true
  #
  #     @point_two = RTanque::Point.new(100, 1, @arena)
  #     # => #<struct RTanque::Point x=100, y=1, arena=#<struct RTanque::Arena width=100, height=100>>
  #
  #     @point_two.within_radius?(@point_one, 10)
  #     # => false
  #
  #     @point_two.within_radius?(@point_one, 100)
  #     # => true
  #
  #     @point_two.distance(@point_one)
  #     # => 100.0
  #
  # @attr_reader [Numeric] x horizontal position (left edge is 0)
  # @attr_reader [Numeric] y vertical position (bottom edge is 0)
  # @attr_reader [RTanque::Arena] arena
  #
  # @!method distance(other_point) distance to other point
  #    @param [RTanque::Point]
  #    @return [Float]
  #
  # @!method heading(other_point) heading to other point
  #    @param [RTanque::Point]
  #    @return [RTanque::Heading]
  #
  # @!method on_top_wall?
  #    @return [Boolean]
  #
  # @!method on_bottom_wall?
  #    @return [Boolean]
  #
  # @!method on_right_wall?
  #    @return [Boolean]
  #
  # @!method on_left_wall?
  #    @return [Boolean]
  #
  # @!method on_wall?
  #    True if on any wall
  #    @return [Boolean]
  Point = Struct.new(:x, :y, :arena) do
    def initialize(*args, &block)
      super
      block.call(self) if block
      self.freeze
    end

    def self.rand(arena)
      self.new(Kernel.rand(arena.width), Kernel.rand(arena.height), arena)
    end

    def self.distance(a, b)
      Math.hypot(a.x - b.x, a.y - b.y)
    end

    def ==(other_point)
      self.x == other_point.x && self.y == other_point.y
    end

    def within_radius?(other_point, radius)
      self.distance(other_point) <= radius
    end

    def on_top_wall?
      self.y >= self.arena.height
    end

    def on_bottom_wall?
      self.y <= 0
    end

    def on_left_wall?
      self.x <= 0
    end

    def on_right_wall?
      self.x >= self.arena.width
    end

    def on_wall?
      self.on_top_wall? || self.on_bottom_wall? || self.on_right_wall? || self.on_left_wall?
    end

    def outside_arena?
      self.y > self.arena.height || self.y < 0 || self.x > self.arena.width || self.x < 0
    end

    def move(heading, speed, bound_to_arena = true)
      # round to avoid floating point errors
      x = (self.x + (Math.sin(heading) * speed)).round(10)
      y = (self.y + (Math.cos(heading) * speed)).round(10)
      self.class.new(x, y, self.arena) { |point| point.bind_to_arena if bound_to_arena }
    end

    def bind_to_arena
      if self.x < 0
        self.x = 0.0
      elsif self.x > self.arena.width
        self.x = self.arena.width.to_f
      end
      if self.y < 0
        self.y = 0.0
      elsif self.y > self.arena.height
        self.y = self.arena.height.to_f
      end
    end

    def heading(other_point)
      Heading.new_between_points(self, other_point)
    end

    def distance(other_point)
      self.class.distance(self, other_point)
    end
  end
end