# frozen-string-literal: true
#
# The pg_range_ops extension adds support to Sequel's DSL to make
# it easier to call PostgreSQL range and multirange functions and operators.
#
# To load the extension:
#
#   Sequel.extension :pg_range_ops
#
# The most common usage is passing an expression to Sequel.pg_range_op:
#
#   r = Sequel.pg_range_op(:range)
#
# If you have also loaded the pg_range or pg_multirange extensions, you can use
# Sequel.pg_range or Sequel.pg_multirange as well:
#
#   r = Sequel.pg_range(:range)
#   r = Sequel.pg_multirange(:range)
#
# Also, on most Sequel expression objects, you can call the pg_range
# method:
#
#   r = Sequel[:range].pg_range
#
# If you have loaded the {core_extensions extension}[rdoc-ref:doc/core_extensions.rdoc],
# or you have loaded the core_refinements extension
# and have activated refinements for the file, you can also use Symbol#pg_range:
#
#   r = :range.pg_range
#
# This creates a Sequel::Postgres::RangeOp object that can be used
# for easier querying:
#
#   r.contains(:other)      # range @> other
#   r.contained_by(:other)  # range <@ other
#   r.overlaps(:other)      # range && other
#   r.left_of(:other)       # range << other
#   r.right_of(:other)      # range >> other
#   r.starts_after(:other)  # range &> other
#   r.ends_before(:other)   # range &< other
#   r.adjacent_to(:other)   # range -|- other
#
#   r.lower            # lower(range)
#   r.upper            # upper(range)
#   r.isempty          # isempty(range)
#   r.lower_inc        # lower_inc(range)
#   r.upper_inc        # upper_inc(range)
#   r.lower_inf        # lower_inf(range)
#   r.upper_inf        # upper_inf(range)
#
# All of the above methods work for both ranges and multiranges, as long
# as PostgreSQL supports the operation. The following methods are also
# supported:
#
#   r.range_merge      # range_merge(range)
#   r.unnest           # unnest(range)
#   r.multirange       # multirange(range)
#
# +range_merge+ and +unnest+ expect the receiver to represent a multirange
# value, while +multi_range+ expects the receiver to represent a range value.
#   
# See the PostgreSQL range and multirange function and operator documentation for more
# details on what these functions and operators do.
#
# If you are also using the pg_range or pg_multirange extension, you should
# load them before loading this extension.  Doing so will allow you to use
# PGRange#op and PGMultiRange#op to get a RangeOp, allowing you to perform
# range operations on range literals.
#
# Related module: Sequel::Postgres::RangeOp

#
module Sequel
  module Postgres
    # The RangeOp class is a simple container for a single object that
    # defines methods that yield Sequel expression objects representing
    # PostgreSQL range operators and functions.
    #
    # Most methods in this class are defined via metaprogramming, see
    # the pg_range_ops extension documentation for details on the API.
    class RangeOp < Sequel::SQL::Wrapper
      OPERATORS = {
        :contains => ["(".freeze, " @> ".freeze, ")".freeze].freeze,
        :contained_by => ["(".freeze, " <@ ".freeze, ")".freeze].freeze,
        :left_of => ["(".freeze, " << ".freeze, ")".freeze].freeze,
        :right_of => ["(".freeze, " >> ".freeze, ")".freeze].freeze,
        :ends_before => ["(".freeze, " &< ".freeze, ")".freeze].freeze,
        :starts_after => ["(".freeze, " &> ".freeze, ")".freeze].freeze,
        :adjacent_to => ["(".freeze, " -|- ".freeze, ")".freeze].freeze,
        :overlaps => ["(".freeze, " && ".freeze, ")".freeze].freeze,
      }.freeze

      %w'lower upper isempty lower_inc upper_inc lower_inf upper_inf unnest'.each do |f|
        class_eval("def #{f}; function(:#{f}) end", __FILE__, __LINE__)
      end
      %w'range_merge multirange'.each do |f|
        class_eval("def #{f}; RangeOp.new(function(:#{f})) end", __FILE__, __LINE__)
      end
      OPERATORS.each_key do |f|
        class_eval("def #{f}(v); operator(:#{f}, v) end", __FILE__, __LINE__)
      end

      # These operators are already supported by the wrapper, but for ranges they
      # return ranges, so wrap the results in another RangeOp.
      %w'+ * -'.each do |f|
        class_eval("def #{f}(v); RangeOp.new(super) end", __FILE__, __LINE__)
      end

      # Return the receiver.
      def pg_range
        self
      end

      private

      # Create a boolen expression for the given type and argument.
      def operator(type, other)
        Sequel::SQL::BooleanExpression.new(:NOOP, Sequel::SQL::PlaceholderLiteralString.new(OPERATORS[type], [value, other]))
      end

      # Return a function called with the receiver.
      def function(name)
        Sequel::SQL::Function.new(name, self)
      end
    end

    module RangeOpMethods
      # Wrap the receiver in an RangeOp so you can easily use the PostgreSQL
      # range functions and operators with it.
      def pg_range
        RangeOp.new(self)
      end
    end

    # :nocov:
    if defined?(PGRange)
    # :nocov:
      class PGRange
        # Wrap the PGRange instance in an RangeOp, allowing you to easily use
        # the PostgreSQL range functions and operators with literal ranges.
        def op
          RangeOp.new(self)
        end
      end
    end

    # :nocov:
    if defined?(PGMultiRange)
    # :nocov:
      class PGMultiRange
        # Wrap the PGRange instance in an RangeOp, allowing you to easily use
        # the PostgreSQL range functions and operators with literal ranges.
        def op
          RangeOp.new(self)
        end
      end
    end
  end

  module SQL::Builders
    # Return the expression wrapped in the Postgres::RangeOp.
    def pg_range_op(v)
      case v
      when Postgres::RangeOp
        v
      else
        Postgres::RangeOp.new(v)
      end
    end
  end

  class SQL::GenericExpression
    include Sequel::Postgres::RangeOpMethods
  end

  class LiteralString
    include Sequel::Postgres::RangeOpMethods
  end
end

# :nocov:
if Sequel.core_extensions?
  class Symbol
    include Sequel::Postgres::RangeOpMethods
  end
end

if defined?(Sequel::CoreRefinements)
  module Sequel::CoreRefinements
    refine Symbol do
      send INCLUDE_METH, Sequel::Postgres::RangeOpMethods
    end
  end
end
# :nocov:
