module Ray
  # Implementation of turtle graphics.
  #
  # A turtle keeps the following caracteristics:
  # - An image which it is manipulating.
  # - Its position (x, y), relative to the image. Set to (0, 0) by default.
  # - An angle, in radians or in degrees (using degrees by default).
  #   Set to 0 by default, i.e. heading East.
  # - Whether the turtle should draw when moved (defaults to true)
  # - The color in which lines will be drawn. Defaults to white.
  class Turtle
    include Math

    # @param [Ray::Target, Ray::image] target the object on which the turtle
    #   should draw. If target is an image, an ImageTarget will be created.
    def initialize(target)
      @target  = target.is_a?(Image) ? Ray::ImageTarget.new(target) : target
      @drawing = true

      @pos = Ray::Vector2[0, 0]
      @angle = 0 # in rad

      @angle_unit = :deg

      @pen_width = 1

      @color = Ray::Color.white
    end

    # Stops drawing
    def pen_up
      @drawing = false
    end

    # Starts drawing
    def pen_down
      @drawing = true
    end

    # @return Whether the turtle is drawing
    def drawing?
      @drawing
    end

    alias pen_down? drawing?

    # @return The opposite of #pen_down?
    def pen_up?
      !@drawing
    end

    # Moves the turtle in the direction it's facing.
    # Draws a line if the pen is down.
    #
    # @param [Float] dist Length of the line
    # @see #backward
    def forward(dist)
      x = pos.x + dist * cos(@angle)
      y = pos.y - dist * sin(@angle) # The y-axis is reversed

      go_to [x, y]
    end

    # Same as forward(-dist)
    # @see #forward
    def backward(dist)
      forward(-dist)
    end

    # Turns the turtle counter-clockwise.
    # @param [Float] angle The angle, in degrees if the angle unit
    #                      is set to degrees. In radians otherwise.
    # @see #right
    def left(angle)
      self.angle += angle
    end

    # Turns the turtle clockwise.
    # @param [Float] angle The angle, in degrees if the angle unit
    #                      is set to degrees. In radians otherwise.
    # @see #left
    def right(angle)
      self.angle -= angle
    end

    # Makes the turtle move to (x, y). Draws if the pen is down.
    def go_to(point)
      if pen_down?
        @target.draw Ray::Polygon.line(pos, point, @pen_width, @color)
      end

      self.pos = point
    end

    # Makes the turtle go to the center. Draws if the pen is down.
    def center
      go_to @target.clip.center
    end

    # Resets the turtle's position, angle, and color but not
    # the content of the image. Also sets the pen down.
    # @see #clear
    def reset
      self.angle     = 0
      self.pos       = [0, 0]
      self.color     = Ray::Color.white
      self.pen_width = 1

      pen_down
    end

    # Clears the content of the image and call reset.
    # @see #reset
    def clear
      @target.clear Ray::Color.none
      reset
    end

    # Returns the distance between the turtle and a point.
    # @see #dist_square
    def dist(point)
      sqrt dist_square(point)
    end

    # Returns the square of the distance between the turtle and a point.
    # Since this doesn't require to call sqrt, it is faster to use this
    # when possible.
    #
    # @see #dist
    def dist_square(point)
      (pos - point.to_vector2).length
    end

    # @return [Ray::Target] The target the turtle is drawing on
    attr_reader :target

    def x
      pos.x
    end

    def y
      pos.y
    end

    def x=(val)
      self.pos = [val, y]
    end

    def y=(val)
      self.pos = [x, val]
    end

    # Sets the position of the turtle.
    # @example
    #   turtle.pos = [10, 20]
    #   turtle.pos = Ray::Vector2[10, 20]
    def pos=(point)
      @pos = point.to_vector2
    end

    attr_reader :pos

    # The unit used for angle. Either :deg (degrees) or :rad
    # (radians)
    attr_reader :angle_unit

    # Sets the angle unit.
    def angle_unit=(unit)
      unless unit == :deg || unit == :rad
        raise ArgumentError, "Unknown angle unit '#{unit}'"
      end

      @angle_unit = unit
    end

    # @return [Float] The angle, either in radians or in degrees,
    #                 depending on angle_unit.
    def angle
      @angle_unit == :deg ? rad_to_deg(@angle) : @angle
    end

    # Sets the angle.
    # @param [Float] val The angle, either in radians or in degrees,
    #                 depending on angle_unit.
    def angle=(val)
      @angle = @angle_unit == :deg ? deg_to_rad(val) : val
    end

    # The color the turtle uses for drawing.
    attr_accessor :color
    alias pen_color color=

    attr_accessor :pen_width

    private
    def deg_to_rad(deg)
      (PI * deg) / 180
    end

    def rad_to_deg(rad)
      (180 * rad) / PI
    end
  end

  class Image
    # (see Ray::Target#turtle)
    def turtle(&block)
      Ray::ImageTarget.new(self).turtle(&block)
    end
  end

  class Target
    # Creates a turtle operating on the receiver
    #
    # If a block is passed, it is instance-evaluated (self becoming the turtle)
    # and the receiver gets updated automatically.
    #
    # @yield Optionally perform drawing in the block
    #
    # @return [Ray::Turtle] The turtle it created
    # @see Ray::Turtle
    def turtle(&block)
      turtle = Ray::Turtle.new(self)
      if block
        turtle.instance_eval(&block)
        update
      end

      turtle
    end
  end
end
