module DataMapper
  module Adapters
    module Sql
      module Commands
    
        class LoadCommand
          
          class Loader

            def initialize(load_command, klass)
              @load_command, @klass = load_command, klass
              @columns = {}
              @key = nil
              @key_index = nil
              @type_override_present = false
              @type_override_index = nil
              @type_override = nil
              @database_context = load_command.database_context
              @reload = load_command.reload?
              @set = []
            end

            def add_column(column, index)
              if column.key?
                @key = column 
                @key_index = index
              end

              if column.type == :class
                @type_override_present = true
                @type_override_index = index
                @type_override = column
              end

              @columns[index] = column

              self
            end

            def materialize(values)

              instance_id = @key.type_cast_value(values[@key_index])
              instance = create_instance(instance_id,
                if @type_override_present
                  @type_override.type_cast_value(values[@type_override_index]) || @klass
                else
                  @klass
                end
              )

              @klass.callbacks.execute(:before_materialize, instance)

              original_values = instance.original_values

              @columns.each_pair do |index, column|
                # This may be a little confusing, but we're
                # setting both the original_value, and the
                # instance-variable through method chaining to avoid
                # lots of extra short-lived local variables.
                type_cast_value = instance.instance_variable_set(
                  column.instance_variable_name,
                  column.type_cast_value(values[index])
                )
                
                original_values[column.name] = case type_cast_value
                  when String, Date, Time then type_cast_value.dup
                  when column.type == :object then Marshal.dump(type_cast_value)
                  else type_cast_value
                end
              end

              instance.instance_variable_set(:@loaded_set, @set)
              @set << instance

              @klass.callbacks.execute(:after_materialize, instance)

              return instance
              
            rescue => e
              raise MaterializationError.new("Failed to materialize row: #{values.inspect}\n#{e.to_yaml}")
            end

            def loaded_set
              @set
            end

            private
              
              class MaterializationError < StandardError; end
                
              def create_instance(instance_id, instance_type)
                instance = @database_context.identity_map.get(@klass, instance_id)

                if instance.nil? || @reload
                  instance = instance_type.new() if instance.nil?
                  instance.instance_variable_set(:@__key, instance_id)
                  instance.instance_variable_set(:@new_record, false)
                  @database_context.identity_map.set(instance)
                elsif instance.new_record?
                  instance.instance_variable_set(:@__key, instance_id)
                  instance.instance_variable_set(:@new_record, false)
                end

                instance.database_context = @database_context

                return instance
              end

          end
          
          class ConditionsError < StandardError

            attr_reader :inner_error

            def initialize(clause, value, inner_error)
              @clause, @value, @inner_error = clause, value, inner_error
            end

            def message
              "Conditions (:clause => #{@clause.inspect}, :value => #{@value.inspect}) failed: #{@inner_error}"
            end

            def backtrace
              @inner_error.backtrace
            end              

          end
          
          attr_reader :conditions, :database_context, :options
          
          def initialize(adapter, database_context, primary_class, options = {})
            @adapter, @database_context, @primary_class = adapter, database_context, primary_class
            
            # BEGIN: Partion out the options hash into general options,
            # and conditions.
            standard_find_options = @adapter.class::FIND_OPTIONS
            conditions_hash = {}
            @options = {}
            
            options.each do |key,value|
              if standard_find_options.include?(key) && key != :conditions
                @options[key] = value
              else
                conditions_hash[key] = value
              end
            end
            # END
            
            @order = @options[:order]
            @limit = @options[:limit]
            @offset = @options[:offset]
            @reload = @options[:reload]
            @instance_id = conditions_hash[:id]
            @conditions = parse_conditions(conditions_hash)
            @loaders = Hash.new { |h,k| h[k] = Loader.new(self, k) }
          end
          
          # Display an overview of load options at a glance.          
          def inspect
            <<-EOS.compress_lines % (object_id * 2)
              #<#{self.class.name}:0x%x
                @database=#{@adapter.name}
                @reload=#{@reload.inspect}
                @order=#{@order.inspect}
                @limit=#{@limit.inspect}
                @offset=#{@offset.inspect}
                @options=#{@options.inspect}>
            EOS
          end
                              
          # Access the Conditions instance
          def conditions
            @conditions
          end
          
          # If +true+ then force the command to reload any objects
          # already existing in the IdentityMap when executing.
          def reload?
            @reload
          end
          
          # Determine if there is a limitation on the number of
          # instances returned in the results. If +nil+, no limit
          # is set. Can be used in conjunction with #offset for
          # paging through a set of results.
          def limit
            @limit
          end
          
          # Used in conjunction with #limit to page through a set
          # of results.
          def offset
            @offset
          end
          
          def call
            
            # Check to see if the query is for a specific id and return if found
            #
            # NOTE: If the :id option is an Array:
            # We could search for loaded instance ids and reject from
            # the Array for already loaded instances, but working under the
            # assumption that we'll probably have to issue a query to find
            # at-least some of the instances we're looking for, it's faster to
            # just skip that and go straight for the query.
            unless reload? || @instance_id.blank? || @instance_id.is_a?(Array)
              # If the id is for only a single record, attempt to find it.
              if instance = @database_context.identity_map.get(@primary_class, @instance_id)
                return [instance]
              end
            end
            
            results = []
            
            # Execute the statement and load the objects.
            @adapter.connection do |db|
              sql, *parameters = to_parameterized_sql
              command = db.create_command(sql)
              command.execute_reader(*parameters) do |reader|
                if @options.has_key?(:intercept_load)
                  load(reader, &@options[:intercept_load])
                else
                  load(reader)
                end
              end
            end
            
            results += @loaders[@primary_class].loaded_set
            
            return results
          end
          
          def load(reader)          
            # The following blocks are identical aside from the yield.
            # It's written this way to avoid a conditional within each
            # iterator, and to take advantage of the performance of
            # yield vs. Proc#call.
            if block_given?
              reader.each do
                @loaders.each_pair do |klass,loader|
                  row = reader.current_row
                  yield(loader.materialize(row), @columns, row)
                end
              end
            else
              reader.each do
                @loaders.each_pair do |klass,loader|
                  loader.materialize(reader.current_row)
                end
              end
            end
          end
          
          # Are any conditions present?
          def conditions_empty?
            @conditions.empty?
          end
          
          # Generate a select statement based on the initialization
          # arguments.
          def to_parameterized_sql
            parameters = []
            
            sql = 'SELECT ' << columns_for_select.join(', ')
            sql << ' FROM ' << from_table_name            
            
            included_associations.each do |association|
              sql << ' ' << association.to_sql
            end
            
            shallow_included_associations.each do |association|
              sql << ' ' << association.to_shallow_sql
            end
            
            unless conditions_empty?
              sql << ' WHERE ('
              
              last_index = @conditions.size
              current_index = 0
              
              @conditions.each do |condition|
                case condition
                when String then sql << condition
                when Array then
                    sql << condition.shift
                    parameters += condition
                else
                  raise "Unable to parse condition: #{condition.inspect}" if condition
                end
                
                if (current_index += 1) == last_index
                  sql << ')'
                else
                  sql << ') AND ('
                end
              end
            end # unless conditions_empty?
            
            unless @order.nil?
              sql << ' ORDER BY ' << @order.to_s
            end
        
            unless @limit.nil?
              sql << ' LIMIT ' << @limit.to_s
            end
            
            unless @offset.nil?
              sql << ' OFFSET ' << @offset.to_s
            end
            
            parameters.unshift(sql)
          end
          
          # If more than one table is involved in the query, the column definitions should
          # be qualified by the table name. ie: people.name
          # This method determines wether that needs to happen or not.
          # Note: After the first call, the calculations are avoided by overwriting this
          # method with a simple getter.
          def qualify_columns?
            @qualify_columns = !(included_associations.empty? && shallow_included_associations.empty?)
            def self.qualify_columns?
              @qualify_columns
            end
            @qualify_columns
          end
          
          # expression_to_sql takes a set of arguments, and turns them into a an
          # Array of generated SQL, followed by optional Values to interpolate as SQL-Parameters.
          #
          # Parameters:
          # +clause+ The name of the column as a Symbol, a raw-SQL String, a Mappings::Column
          # instance, or a Symbol::Operator.
          # +value+ The Value for the condition.
          # +collector+ An Array representing all conditions that is appended to by expression_to_sql
          #
          # Returns: Undefined Output. The work performed is added to the +collector+ argument.
          # Example:
          #   conditions = []
          #   expression_to_sql(:name, 'Bob', conditions)
          #   => +undefined return value+
          #   conditions.inspect
          #   => ["name = ?", 'Bob']
          def expression_to_sql(clause, value, collector)
            qualify_columns = qualify_columns?

            case clause
            when Symbol::Operator then
              operator = case clause.type
              when :gt then '>'
              when :gte then '>='
              when :lt then '<'
              when :lte then '<='
              when :not then inequality_operator(value)
              when :eql then equality_operator(value)
              when :like then equality_operator(value, 'LIKE')
              when :in then equality_operator(value)
              else raise ArgumentError.new('Operator type not supported')
              end
              collector << ["#{primary_class_table[clause].to_sql(qualify_columns)} #{operator} ?", value]
            when Symbol then
              collector << ["#{primary_class_table[clause].to_sql(qualify_columns)} #{equality_operator(value)} ?", value]
            when String then
              collector << [clause, *value]
            when Mappings::Column then
              collector << ["#{clause.to_sql(qualify_columns)} #{equality_operator(value)} ?", value]
            else raise "CAN HAS CRASH? #{clause.inspect}"
            end
          rescue => e
            raise ConditionsError.new(clause, value, e)
          end
          
          private            
            # Return the Sql-escaped columns names to be selected in the results.
            def columns_for_select
              @columns_for_select || begin
                qualify_columns = qualify_columns?
                @columns_for_select = []
                
                columns.each_with_index do |column,i|
                  class_for_loader = column.table.klass
                  @loaders[class_for_loader].add_column(column, i) if class_for_loader
                  @columns_for_select << column.to_sql(qualify_columns)
                end
                
                @columns_for_select
              end
              
            end
            
            # Returns the DataMapper::Adapters::Sql::Mappings::Column instances to
            # be selected in the results.
            def columns
              @columns || begin
                @columns = primary_class_columns
                @columns += included_columns
                
                included_associations.each do |assoc|
                  @columns += assoc.associated_columns
                end
                
                shallow_included_associations.each do |assoc|
                  @columns += assoc.join_columns
                end
                
                @columns
              end
            end
            
            # Returns the default columns for the primary_class_table,
            # or maps symbols specified in a +:select+ option to columns
            # in the primary_class_table.
            def primary_class_columns
              @primary_class_columns || @primary_class_columns = begin
                if @options.has_key?(:select)
                  case x = @options[:select]
                  when Array then x
                  when Symbol then [x]
                  else raise ':select option must be a Symbol, or an Array of Symbols'
                  end.map { |name| primary_class_table[name] }
                else
                  primary_class_table.columns.reject { |column| column.lazy? }
                end
              end
            end
            
            def included_associations
              @included_associations || @included_associations = begin
                associations = primary_class_table.associations
                include_options.map do |name|
                  associations[name]
                end.compact
              end
            end
            
            def shallow_included_associations
              @shallow_included_associations || @shallow_included_associations = begin
                associations = primary_class_table.associations
                shallow_include_options.map do |name|
                  associations[name]
                end.compact
              end
            end
            
            def included_columns
              @included_columns || @included_columns = begin
                include_options.map do |name|
                  primary_class_table[name]
                end.compact
              end
            end
            
            def include_options
              @include_options || @include_options = begin
                case x = @options[:include]
                when Array then x
                when Symbol then [x]
                else []
                end
              end
            end
            
            def shallow_include_options
              @shallow_include_options || @shallow_include_options = begin
                case x = @options[:shallow_include]
                when Array then x
                when Symbol then [x]
                else []
                end
              end
            end
            
            # Determine if a Column should be included based on the
            # value of the +:include+ option.
            def include_column?(name)
              !primary_class_table[name].lazy? || include_options.includes?(name)
            end

            # Return the Sql-escaped table name of the +primary_class+.
            def from_table_name
              @from_table_name || (@from_table_name = @adapter.table(@primary_class).to_sql)
            end
            
            # Returns the DataMapper::Adapters::Sql::Mappings::Table for the +primary_class+.
            def primary_class_table
              @primary_class_table || (@primary_class_table = @adapter.table(@primary_class))
            end
            
            def parse_conditions(conditions_hash)
              collection = []

              case x = conditions_hash.delete(:conditions)
              when Array then
                clause = x.shift
                expression_to_sql(clause, x, collection)
              when Hash then
                x.each_pair do |key,value|
                  expression_to_sql(key, value, collection)
                end
              else
                raise "Unable to parse conditions: #{x.inspect}" if x
              end
              
              if primary_class_table.paranoid?
                conditions_hash[primary_class_table.paranoid_column.name] = nil
              end
              
              conditions_hash.each_pair do |key,value|
                expression_to_sql(key, value, collection)
              end

              collection              
            end

            def equality_operator(value, default = '=')
              case value
              when NilClass then 'IS'
              when Array then 'IN'
              else default
              end
            end

            def inequality_operator(value, default = '<>')
              case value
              when NilClass then 'IS NOT'
              when Array then 'NOT IN'
              else default
              end
            end
          
        end # class LoadCommand
      end # module Commands
    end # module Sql
  end # module Adapters
end # module DataMapper