# frozen-string-literal: true

module Sequel
  module Plugins
    # The lazy_attributes plugin allows users to easily set that some attributes
    # should not be loaded by default when loading model objects.  If the attribute
    # is needed after the instance has been retrieved, a database query is made to
    # retreive the value of the attribute.
    #
    # This plugin depends on the tactical_eager_loading plugin, and allows you to
    # eagerly load lazy attributes for all objects retrieved with the current object.
    # So the following code should issue one query to get the albums and one query to
    # get the reviews for all of those albums:
    #
    #   Album.plugin :lazy_attributes, :review
    #   Album.where{id < 100}.all do |a|
    #     a.review
    #   end
    #
    #   # You can specify multiple columns to lazily load:
    #   Album.plugin :lazy_attributes, :review, :tracklist
    #
    # Note that by default on databases that supporting RETURNING,
    # using explicit column selections will cause instance creations
    # to use two queries (insert and refresh) instead of a single
    # query using RETURNING.  You can use the insert_returning_select
    # plugin to automatically use RETURNING for instance creations
    # for models using the lazy_attributes plugin.
    module LazyAttributes
      # Lazy attributes requires the tactical_eager_loading plugin
      def self.apply(model, *attrs)
        model.plugin :tactical_eager_loading  
      end
      
      # Set the attributes given as lazy attributes
      def self.configure(model, *attrs)
        model.lazy_attributes(*attrs) unless attrs.empty?
      end
      
      module ClassMethods
        # Freeze lazy attributes module when freezing model class.
        def freeze
          @lazy_attributes_module.freeze if @lazy_attributes_module

          super
        end

        # Remove the given attributes from the list of columns selected by default.
        # For each attribute given, create an accessor method that allows a lazy
        # lookup of the attribute.  Each attribute should be given as a symbol.
        def lazy_attributes(*attrs)
          unless select = dataset.opts[:select]
            select = dataset.columns.map{|c| Sequel.qualify(dataset.first_source, c)}
          end
          db_schema = @db_schema
          set_dataset(dataset.select(*select.reject{|c| attrs.include?(dataset.send(:_hash_key_symbol, c))}))
          @db_schema = db_schema
          attrs.each{|a| define_lazy_attribute_getter(a)}
        end
        
        private

        # Add a lazy attribute getter method to the lazy_attributes_module. Options:
        # :dataset :: The base dataset to use for the lazy attribute lookup
        # :table :: The table name to use to qualify the attribute and primary key columns.
        def define_lazy_attribute_getter(a, opts=OPTS)
          include(@lazy_attributes_module ||= Module.new) unless @lazy_attributes_module
          @lazy_attributes_module.class_eval do
            define_method(a) do
              if !values.has_key?(a) && !new?
                lazy_attribute_lookup(a, opts)
              else
                super()
              end
            end
            alias_method(a, a)
          end
        end
      end

      module InstanceMethods
        private

        # If the model was selected with other model objects, eagerly load the
        # attribute for all of those objects.  If not, query the database for
        # the attribute for just the current object.  Return the value of
        # the attribute for the current object.
        def lazy_attribute_lookup(a, opts=OPTS)
          table = opts[:table] || model.table_name
          selection = Sequel.qualify(table, a)

          if base_ds = opts[:dataset]
            ds = base_ds.where(qualified_pk_hash(table))
          else
            base_ds = model.dataset
            ds = this
          end

          if frozen?
            return ds.get(selection)
          end

          if retrieved_with
            primary_key = model.primary_key
            composite_pk = true if primary_key.is_a?(Array)
            id_map = {}
            retrieved_with.each{|o| id_map[o.pk] = o unless o.values.has_key?(a) || o.frozen?}
            predicate_key = composite_pk ? primary_key.map{|k| Sequel.qualify(table, k)} : Sequel.qualify(table, primary_key)
            base_ds.
             select(*(Array(primary_key).map{|k| Sequel.qualify(table, k)} + [selection])).
             where(predicate_key=>id_map.keys).
             naked.
             each do |row|
              obj = id_map[composite_pk ? row.values_at(*primary_key) : row[primary_key]]
              if obj && !obj.values.has_key?(a)
                obj.values[a] = row[a]
              end
            end
          end
          values[a] = ds.get(selection) unless values.has_key?(a)
          values[a]
        end
      end
    end
  end
end
