module Hobo
  module Model
    module Scopes

      module AutomaticScopes

        def create_automatic_scope(name, check_only=false)
          ScopeBuilder.new(self, name).create_scope(check_only)
        rescue ActiveRecord::StatementInvalid => e
          # Problem with the database? Don't try to create automatic
          # scopes
          if ActiveRecord::Base.logger
            ActiveRecord::Base.logger.warn "!! Database exception during hobo auto-scope creation -- continuing automatic scopes"
            ActiveRecord::Base.logger.warn "!! #{e.to_s}"
          end
          false
        end

      end

      # The methods on this module add scopes to the given class
      class ScopeBuilder

        def initialize(klass, name)
          @klass = klass
          @name  = name.to_s
        end

        attr_reader :name

        def create_scope(check_only=false)
          matched_scope = true

          case
          # --- Association Queries --- #

          # with_players(player1, player2)
          when name =~ /^with_(.*)/ && (refl = reflection($1))
            return true if check_only

            def_scope do |*records|
              if records.empty?
                @klass.where exists_sql_condition(refl, true)
              else
                records = records.flatten.compact.map {|r| find_if_named(refl, r) }
                exists_sql = ([exists_sql_condition(refl)] * records.length).join(" AND ")
                @klass.where *([exists_sql] + records)
              end
            end

          # with_player(a_player)
          when name =~ /^with_(.*)/ && (refl = reflection($1.pluralize))
            return true if check_only

            exists_sql = exists_sql_condition(refl)
            def_scope do |record|
              record = find_if_named(refl, record)
              @klass.where exists_sql, record
            end

          # any_of_players(player1, player2)
          when name =~ /^any_of_(.*)/ && (refl = reflection($1))
            return true if check_only

            def_scope do |*records|
              if records.empty?
                @klass.where exists_sql_condition(refl, true)
              else
                records = records.flatten.compact.map {|r| find_if_named(refl, r) }
                exists_sql = ([exists_sql_condition(refl)] * records.length).join(" OR ")
                @klass.where *([exists_sql] + records)
              end
            end

          # without_players(player1, player2)
          when name =~ /^without_(.*)/ && (refl = reflection($1))
            return true if check_only

            def_scope do |*records|
              if records.empty?
                @klass.where "NOT (#{exists_sql_condition(refl, true)})"
              else
                records = records.flatten.compact.map {|r| find_if_named(refl, r) }
                exists_sql = ([exists_sql_condition(refl)] * records.length).join(" AND ")
                @klass.where *(["NOT (#{exists_sql})"] + records)
              end
            end

          # without_player(a_player)
          when name =~ /^without_(.*)/ && (refl = reflection($1.pluralize))
            return true if check_only

            exists_sql = exists_sql_condition(refl)
            def_scope do |record|
              record = find_if_named(refl, record)
              @klass.where "NOT #{exists_sql}", record
            end

          # team_is(a_team)
          when name =~ /^(.*)_is$/ && (refl = reflection($1)) && refl.macro.in?([:has_one, :belongs_to])
            return true if check_only

            if refl.options[:polymorphic]
              def_scope do |record|
                record = find_if_named(refl, record)
                @klass.where "#{foreign_key_column refl} = ? AND #{$1}_type = ?", record, record.class.name
              end
            else
              def_scope do |record|
                record = find_if_named(refl, record)
                @klass.where "#{foreign_key_column refl} = ?", record
              end
            end

          # team_is_not(a_team)
          when name =~ /^(.*)_is_not$/ && (refl = reflection($1)) && refl.macro.in?([:has_one, :belongs_to])
            return true if check_only

            if refl.options[:polymorphic]
              def_scope do |record|
                record = find_if_named(refl, record)
                @klass.where "#{foreign_key_column refl} <> ? OR #{name}_type <> ?", record, record.class.name
              end
            else
              def_scope do |record|
                record = find_if_named(refl, record)
                @klass.where "#{foreign_key_column refl} <> ?", record
              end
            end


          # --- Column Queries --- #

          # name_is(str)
          when name =~ /^(.*)_is$/ && (col = column($1))
            return true if check_only

            def_scope do |str|
              @klass.where "#{column_sql(col)} = ?", str
            end

          # name_is_not(str)
          when name =~ /^(.*)_is_not$/ && (col = column($1))
            return true if check_only

            def_scope do |str|
              @klass.where "#{column_sql(col)} <> ?", str
            end

          # name_contains(str)
          when name =~ /^(.*)_contains$/ && (col = column($1))
            return true if check_only

            def_scope do |str|
              @klass.where "#{column_sql(col)} LIKE ?", "%#{str}%"
            end

          # name_does_not_contain
          when name =~ /^(.*)_does_not_contain$/ && (col = column($1))
            return true if check_only

            def_scope do |str|
              @klass.where "#{column_sql(col)} NOT LIKE ?", "%#{str}%"
            end

          # name_starts(str)
          when name =~ /^(.*)_starts$/ && (col = column($1))
            return true if check_only

            def_scope do |str|
              @klass.where "#{column_sql(col)} LIKE ?", "#{str}%"
            end

          # name_does_not_start
          when name =~ /^(.*)_does_not_start$/ && (col = column($1))
            return true if check_only

            def_scope do |str|
              @klass.where "#{column_sql(col)} NOT LIKE ?", "#{str}%"
            end

          # name_ends(str)
          when name =~ /^(.*)_ends$/ && (col = column($1))
            return true if check_only

            def_scope do |str|
              @klass.where "#{column_sql(col)} LIKE ?", "%#{str}"
            end

          # name_does_not_end(str)
          when name =~ /^(.*)_does_not_end$/ && (col = column($1))
            return true if check_only

            def_scope do |str|
              @klass.where "#{column_sql(col)} NOT LIKE ?", "%#{str}"
            end

          # published (a boolean column)
          when (col = column(name)) && (col.type == :boolean)
            return true if check_only

            def_scope do
              @klass.where "#{column_sql(col)} = ?", true
            end

          # not_published
          when name =~ /^not_(.*)$/ && (col = column($1)) && (col.type == :boolean)
            return true if check_only

            def_scope do
              @klass.where "#{column_sql(col)} <> ?", true
            end

          # published_before(time)
          when name =~ /^(.*)_before$/ && (col = column("#{$1}_at") || column("#{$1}_date") || column("#{$1}_on")) && col.type.in?([:date, :datetime, :time, :timestamp])
            return true if check_only

            def_scope do |time|
              @klass.where "#{column_sql(col)} < ?", time
            end

          # published_after(time)
          when name =~ /^(.*)_after$/ && (col = column("#{$1}_at") || column("#{$1}_date") || column("#{$1}_on")) && col.type.in?([:date, :datetime, :time, :timestamp])
            return true if check_only

            def_scope do |time|
              @klass.where "#{column_sql(col)} > ?", time
            end

          # published_between(time1, time2)
          when name =~ /^(.*)_between$/ && (col = column("#{$1}_at") || column("#{$1}_date") || column("#{$1}_on")) && col.type.in?([:date, :datetime, :time, :timestamp])
            return true if check_only

            def_scope do |time1, time2|
              @klass.where "#{column_sql(col)} >= ? AND #{column_sql(col)} <= ?", time1, time2
            end

           # active (a lifecycle state)
          when @klass.has_lifecycle? && name.to_sym.in?(@klass::Lifecycle.state_names)
            return true if check_only

            if @klass::Lifecycle.state_names.length == 1
              # nothing to check for - create a dummy scope
              def_scope { @klass.scoped }
              true
            else
              def_scope do
                @klass.where "#{@klass.table_name}.#{@klass::Lifecycle.state_field} = ?", name
              end
            end

          # self is / is not
          when name == "is"
            return true if check_only

            def_scope do |record|
              @klass.where "#{@klass.table_name}.#{@klass.primary_key} = ?", record
            end

          when name == "is_not"
            return true if check_only

            def_scope do |record|
              @klass.where "#{@klass.table_name}.#{@klass.primary_key} <> ?", record
            end


          when name == "by_most_recent"
            return true if check_only

            def_scope do
              @klass.order "#{@klass.table_name}.created_at DESC"
            end

          when name == "recent"
            return true if check_only

            if "created_at".in?(@klass.columns.*.name)
              def_scope do |*args|
                count = args.first || 6
                @klass.order("#{@klass.table_name}.created_at DESC").limit(count)
              end
            else
              def_scope do |*args|
                count = args.first || 6
                limit(count)
              end
            end

          when name == "order_by"
            return true if check_only

            klass = @klass
            def_scope do |*args|
              field, asc = args
              field ||= ""
              type = klass.attr_type(field)
              if type.nil? #a virtual attribute from an SQL alias, e.g., 'total' from 'COUNT(*) AS total'
                colspec = "#{field}" # don't prepend the table name
              elsif type.respond_to?(:name_attribute) && (name = type.name_attribute)
                include = field
                colspec = "#{type.table_name}.#{name}"
              else
                colspec = "#{klass.table_name}.#{field}"
              end
              @klass.includes(include).order("#{colspec} #{asc._?.upcase}")
            end

          when name == "include"
            # DEPRECATED: it clashes with Module.include when called on an ActiveRecord::Relation
            # after a scope chain, if you didn't call it on the class itself first
            Rails.logger.warn "Automatic scope :include has been deprecated: use :includes instead."
            return true if check_only

            def_scope do |inclusions|
              @klass.includes(inclusions)
            end

          when name == "search"
            return true if check_only

            def_scope do |query, *fields|
              match_keyword = %w(PostgreSQL PostGIS).include?(::ActiveRecord::Base.connection.adapter_name) ? "ILIKE" : "LIKE"

              words = (query || "").split
              args = []
              word_queries = words.map do |word|
                field_query = '(' + fields.map { |field|
                  field = "#{@klass.table_name}.#{field}" unless field =~ /\./
                  "(#{field} #{match_keyword} ?)"
                }.join(" OR ") + ')'
                args += ["%#{word}%"] * fields.length
                field_query
              end

              @klass.where *([word_queries.join(" AND ")] + args)
            end

          else
            matched_scope = false
          end

          matched_scope
        end


        def column_sql(column)
          "#{@klass.table_name}.#{column.name}"
        end


        def exists_sql_condition(reflection, any=false)
          owner = @klass
          owner_primary_key = "#{owner.table_name}.#{owner.primary_key}"

          if reflection.options[:through]
            join_table   = reflection.through_reflection.klass.table_name
            owner_fkey   = reflection.through_reflection.foreign_key
            conditions   = reflection.options[:conditions].blank? ? '' : " AND #{reflection.through_reflection.klass.send(:sanitize_sql_for_conditions, reflection.options[:conditions])}"

            if any
              "EXISTS (SELECT * FROM #{join_table} WHERE #{join_table}.#{owner_fkey} = #{owner_primary_key}#{conditions})"
            else
              source_fkey  = reflection.source_reflection.foreign_key
              "EXISTS (SELECT * FROM #{join_table} " +
                "WHERE #{join_table}.#{source_fkey} = ? AND #{join_table}.#{owner_fkey} = #{owner_primary_key}#{conditions})"
            end
          else
            foreign_key = reflection.foreign_key
            related     = reflection.klass
            conditions = reflection.options[:conditions].blank? ? '' : " AND #{reflection.klass.send(:sanitize_sql_for_conditions, reflection.options[:conditions])}"

            if any
              "EXISTS (SELECT * FROM #{related.table_name} " +
                "WHERE #{related.table_name}.#{foreign_key} = #{owner_primary_key}#{conditions})"
            else
              "EXISTS (SELECT * FROM #{related.table_name} " +
                "WHERE #{related.table_name}.#{foreign_key} = #{owner_primary_key} AND " +
                "#{related.table_name}.#{related.primary_key} = ?#{conditions})"
            end
          end
        end

        def find_if_named(reflection, string_or_record)
          if string_or_record.is_a?(String)
            name = string_or_record
            reflection.klass.named(name)
          else
            string_or_record # a record
          end
        end


        def column(name)
          @klass.column(name)
        end


        def reflection(name)
          @klass.reflections[name.to_sym]
        end


        def def_scope(&block)
          @klass.scope name.to_sym, (lambda &block)
        end


        def primary_key_column(refl)
          "#{refl.klass.table_name}.#{refl.klass.primary_key}"
        end


        def foreign_key_column(refl)
          "#{@klass.table_name}.#{refl.foreign_key}"
        end

      end

    end
  end
end