# frozen-string-literal: true
#
# The eval_inspect extension changes #inspect for Sequel::SQL::Expression
# subclasses to return a string suitable for ruby's eval, such that
#
#   eval(obj.inspect) == obj
#
# is true.  The above code is true for most of ruby's simple classes such
# as String, Integer, Float, and Symbol, but it's not true for classes such
# as Time, Date, and BigDecimal.  Sequel attempts to handle situations where
# instances of these classes are a component of a Sequel expression.
#
# To load the extension:
#
#   Sequel.extension :eval_inspect
#
# Related module: Sequel::EvalInspect

#
module Sequel
  module EvalInspect
    # Special case objects where inspect does not generally produce input
    # suitable for eval.  Used by Sequel::SQL::Expression#inspect so that
    # it can produce a string suitable for eval even if components of the
    # expression have inspect methods that do not produce strings suitable
    # for eval.
    def eval_inspect(obj)
      case obj
      when BigDecimal
        "Kernel::BigDecimal(#{obj.to_s.inspect})"
      when Sequel::SQL::Blob, Sequel::LiteralString
        "#{obj.class}.new(#{obj.to_s.inspect})"
      when Sequel::SQL::ValueList
        "#{obj.class}.new(#{obj.to_a.inspect})"
      when Array
        "[#{obj.map{|o| eval_inspect(o)}.join(', ')}]"
      when Set
        "Set[#{obj.map{|o| eval_inspect(o)}.join(', ')}]"
      when Hash
        "{#{obj.map{|k, v| "#{eval_inspect(k)} => #{eval_inspect(v)}"}.join(', ')}}"
      when Time
        datepart = "%Y-%m-%dT" unless obj.is_a?(Sequel::SQLTime)
        "#{obj.class}.parse(#{obj.strftime("#{datepart}%T.%N%z").inspect})#{'.utc' if obj.utc?}"
      when DateTime
        # Ignore date of calendar reform
        "DateTime.parse(#{obj.strftime('%FT%T.%N%z').inspect})"
      when Date
        # Ignore offset and date of calendar reform
        "Date.new(#{obj.year}, #{obj.month}, #{obj.day})"
      else
        obj.inspect
      end
    end
  end

  extend EvalInspect

  module SQL
    class Expression
      alias inspect inspect

      # Attempt to produce a string suitable for eval, such that:
      #
      #   eval(obj.inspect) == obj
      def inspect
        # Assume by default that the object can be recreated by calling
        # self.class.new with any attr_reader values defined on the class,
        # in the order they were defined.
        klass = self.class
        args = inspect_args.map do |arg|
          if arg.is_a?(String) && arg.start_with?('*')
            # Special case string arguments starting with *, indicating that
            # they should return an array to be splatted as the remaining arguments.
            # Allow calling private methods to get inspect output.
            send(arg.sub('*', '')).map{|a| Sequel.eval_inspect(a)}.join(', ')
          else
            # Allow calling private methods to get inspect output.
            Sequel.eval_inspect(send(arg))
          end
        end
        "#{klass}.#{inspect_new_method}(#{args.join(', ')})"
      end

      private

      # Which attribute values to use in the inspect string.
      def inspect_args
        self.class.comparison_attrs
      end

      # Use the new method by default for creating new objects.
      def inspect_new_method
        :new
      end
    end

    class ComplexExpression
      private

      # ComplexExpression's initializer uses a splat for the operator arguments.
      def inspect_args
        [:op, "*args"]
      end
    end

    class Constant
      # Constants to lookup in the Sequel module.
      INSPECT_LOOKUPS = [:CURRENT_DATE, :CURRENT_TIMESTAMP, :CURRENT_TIME, :SQLTRUE, :SQLFALSE, :NULL, :NOTNULL]

      # Reference the constant in the Sequel module if there is
      # one that matches.
      def inspect
        INSPECT_LOOKUPS.each do |c|
          return "Sequel::#{c}" if Sequel.const_get(c) == self
        end
        super
      end
    end

    class CaseExpression
      private

      # CaseExpression's initializer checks whether an argument was
      # provided, to differentiate CASE WHEN from CASE NULL WHEN, so
      # check if an expression was provided, and only include the
      # expression in the inspect output if so.
      def inspect_args
        if expression?
          [:conditions, :default, :expression]
        else
          [:conditions, :default]
        end
      end
    end

    class Function
      private

      # Function uses a new! method for creating functions with options,
      # since Function.new does not allow for an options hash.
      def inspect_new_method
        :new!
      end
    end

    class JoinOnClause
      private

      # JoinOnClause's initializer takes the on argument as the first argument
      # instead of the last.
      def inspect_args
        [:on, :join_type, :table_expr] 
      end
    end

    class JoinUsingClause
      private

      # JoinOnClause's initializer takes the using argument as the first argument
      # instead of the last.
      def inspect_args
        [:using, :join_type, :table_expr] 
      end
    end

    class OrderedExpression
      private

      # OrderedExpression's initializer takes the :nulls information inside a hash,
      # so if a NULL order was given, include a hash with that information.
      def inspect_args
        if nulls
          [:expression, :descending, :opts_hash]
        else
          [:expression, :descending]
        end
      end

      # A hash of null information suitable for passing to the initializer.
      def opts_hash
        {:nulls=>nulls} 
      end
    end
  end
end
