# frozen-string-literal: true

require 'delegate'

module Sequel
  module Plugins
    # The defaults_setter plugin makes the column getter methods return the default
    # values for new objects, if the values have not already been set.  Example:
    #
    #   # column a default NULL
    #   # column b default 2
    #   album = Album.new
    #   album.a # => nil
    #   album.b # => 2
    #   album = Album.new(a: 1, b: 3)
    #   album.a # => 1
    #   album.b # => 3
    #
    # You can manually set default values as well:
    #
    #   Album.default_values[:a] = 4
    #   Album.new.a # => 4
    #
    # You can also provide procs to set default values:
    #
    #   Album.default_values[:a] = lambda{Date.today}
    #   Album.new.a # => Date.today
    #
    # If the proc accepts a single argument, it is passed the instance, allowing
    # default values to depend on instance-specific state:
    #
    #   Album.default_values[:a] = lambda{|album| album.b + 1}
    #   Album.new(b: 10).a # => 11
    #
    # By default, default values returned are not cached:
    #
    #   Album.new.a.equal?(Album.new.a) # => false
    #
    # However, you can turn on caching of default values:
    #
    #   Album.plugin :defaults_setter, cache: true
    #   Album.new.a.equal?(Album.new.a) # => false
    #
    # Note that if the cache is turned on, the cached values are stored in
    # the values hash:
    #
    #   Album.plugin :defaults_setter, cache: true
    #   album = Album.new
    #   album.values # => {}
    #   album.a
    #   album.values # => {:a => Date.today}
    #
    # Usage:
    #
    #   # Make all model subclass instances set defaults (called before loading subclasses)
    #   Sequel::Model.plugin :defaults_setter
    #
    #   # Make the Album class set defaults
    #   Album.plugin :defaults_setter
    module DefaultsSetter
      # Set the default values based on the model schema. Options:
      # :cache :: Cache default values returned in the model's values hash.
      def self.configure(model, opts=OPTS)
        model.instance_exec do
          set_default_values
          @cache_default_values = opts[:cache] if opts.has_key?(:cache)
        end
      end

      module ClassMethods
        # The default values to use for this model.  A hash with column symbol
        # keys and default values.  If the default values respond to +call+, it will be called
        # to get the value, otherwise the value will be used directly.  You can manually modify
        # this hash to set specific default values, by default the ones will be parsed from the database.
        attr_reader :default_values

        Plugins.after_set_dataset(self, :set_default_values)

        Plugins.inherited_instance_variables(self, :@default_values=>:dup, :@cache_default_values=>nil)

        # Whether default values should be cached in the values hash after being retrieved.
        def cache_default_values?
          @cache_default_values
        end

        # Freeze default values when freezing model class
        def freeze
          @default_values.freeze
          super
        end

        private

        # Parse the cached database schema for this model and set the default values appropriately.
        def set_default_values
          h = {}
          if @db_schema
            @db_schema.each do |k, v|
              if v[:callable_default]
                h[k] = v[:callable_default]
              elsif !v[:ruby_default].nil?
                h[k] = convert_default_value(v[:ruby_default])
              end
            end
          end
          @default_values = h.merge!(@default_values || OPTS)
        end

        # Handle the CURRENT_DATE and CURRENT_TIMESTAMP values specially by returning an appropriate Date or
        # Time/DateTime value.
        def convert_default_value(v)
          case v
          when Sequel::CURRENT_DATE
            lambda{Date.today}
          when Sequel::CURRENT_TIMESTAMP
            lambda{dataset.current_datetime}
          when Hash, Array
            v = Marshal.dump(v).freeze
            lambda{Marshal.load(v)}
          when Delegator
            # DelegateClass returns an anonymous case, which cannot be marshalled, so marshal the
            # underlying object and create a new instance of the class with the unmarshalled object.
            klass = v.class
            case o = v.__getobj__
            when Hash, Array
              v = Marshal.dump(o).freeze
              lambda{klass.new(Marshal.load(v))}
            else
              v
            end
          else
            v
          end
        end
      end

      module InstanceMethods
        # Use default value for a new record if values doesn't already contain an entry for it.
        def [](k)
          if new? && !values.has_key?(k)
            v = model.default_values.fetch(k){return}
            if v.respond_to?(:call)
              v = if v.respond_to?(:arity) && v.arity == 1
                v.call(self)
              else
                v.call
              end
            end
            values[k] = v if model.cache_default_values?
            v
          else
            super
          end
        end
      end
    end
  end
end
