# frozen-string-literal: true

module ChunkyPNG
  # Factory method to create {ChunkyPNG::Point} instances.
  #
  # This method tries to be as flexible as possible with regards to the given input: besides
  # explicit coordinates, this method also accepts arrays, hashes, strings, {ChunkyPNG::Dimension}
  # instances and anything that responds to <tt>:x</tt> and <tt>:y</tt>.
  #
  # @overload Point(x, y)
  #   @param [Integer, :to_i] x The x-coordinate
  #   @param [Integer, :to_i] y The y-coordinate
  #   @return [ChunkyPNG::Point] The instantiated point.
  #
  # @overload Point(array)
  #   @param [Array<Integer>] array A two element array which represent the x- and y-coordinate.
  #   @return [ChunkyPNG::Point] The instantiated point.
  #
  # @overload Point(hash)
  #   @param [Hash] array A hash with the <tt>:x</tt> or <tt>'x'</tt> and <tt>:y</tt> or
  #     <tt>'y'</tt> keys set, which will be used as coordinates.
  #   @return [ChunkyPNG::Point] The instantiated point.
  #
  # @overload Point(string)
  #   @param [String] string A string that contains the coordinates, e.g. <tt>'0, 4'</tt>,
  #     <tt>'(0 4)'</tt>, <tt>[0,4}'</tt>, etc.
  #   @return [ChunkyPNG::Point] The instantiated point.
  #
  # @return [ChunkyPNG::Point]
  # @raise [ArgumentError] if the arguments weren't understood.
  # @see ChunkyPNG::Point
  def self.Point(*args)
    case args.length
    when 2 then ChunkyPNG::Point.new(*args)
    when 1 then build_point_from_object(args.first)
    else raise ArgumentError,
      "Don't know how to construct a point from #{args.inspect}!"
    end
  end

  def self.build_point_from_object(source)
    case source
    when ChunkyPNG::Point
      source
    when ChunkyPNG::Dimension
      ChunkyPNG::Point.new(source.width, source.height)
    when Array
      ChunkyPNG::Point.new(source[0], source[1])
    when Hash
      x = source[:x] || source["x"]
      y = source[:y] || source["y"]
      ChunkyPNG::Point.new(x, y)
    when ChunkyPNG::Point::POINT_REGEXP
      ChunkyPNG::Point.new($1.to_i, $2.to_i)
    else
      if source.respond_to?(:x) && source.respond_to?(:y)
        ChunkyPNG::Point.new(source.x, source.y)
      else
        raise ArgumentError,
          "Don't know how to construct a point from #{source.inspect}!"
      end
    end
  end

  private_class_method :build_point_from_object

  # Simple class that represents a point on a canvas using an x and y coordinate.
  #
  # This class implements some basic methods to handle comparison, the splat operator and
  # bounds checking that make it easier to work with coordinates.
  #
  # @see ChunkyPNG.Point
  class Point
    # @return [Regexp] The regexp to parse points from a string.
    # @private
    POINT_REGEXP = /^[\(\[\{]?(\d+)\s*[,]?\s*(\d+)[\)\]\}]?$/

    # @return [Integer] The x-coordinate of the point.
    attr_accessor :x

    # @return [Integer] The y-coordinate of the point.
    attr_accessor :y

    # Initializes a new point instance.
    # @param [Integer, :to_i] x The x-coordinate.
    # @param [Integer, :to_i] y The y-coordinate.
    def initialize(x, y)
      @x, @y = x.to_i, y.to_i
    end

    # Checks whether 2 points are identical.
    # @return [true, false] <tt>true</tt> iff the x and y coordinates match
    def eql?(other)
      other.x == x && other.y == y
    end

    alias == eql?

    # Compares 2 points.
    #
    # It will first compare the y coordinate, and it only takes the x-coordinate into
    # account if the y-coordinates of the points are identical. This way, an array of
    # points will be sorted into the order in which they would occur in the pixels
    # array returned by {ChunkyPNG::Canvas#pixels}.
    #
    # @param [ChunkyPNG::Point] other The point to compare this point with.
    # @return [-1, 0, 1] <tt>-1</tt> If this point comes before the other one, <tt>1</tt>
    #   if after, and <tt>0</tt> if the points are identical.
    def <=>(other)
      (y <=> other.y) == 0 ? x <=> other.x : y <=> other.y
    end

    # Converts the point instance to an array.
    # @return [Array] A 2-element array, i.e. <tt>[x, y]</tt>.
    def to_a
      [x, y]
    end

    alias to_ary to_a

    # Checks whether the point falls into a dimension
    # @param [ChunkyPNG::Dimension, ...] dimension_like The dimension of which the bounds
    #   should be taken for the check.
    # @return [true, false] <tt>true</tt> iff the x and y coordinate fall width the width
    #   and height of the dimension.
    def within_bounds?(*dimension_like)
      ChunkyPNG::Dimension(*dimension_like).include?(self)
    end
  end
end
