module Bullet
  class Association
    class <<self
      @@logger_file = File.open(Bullet::BulletLogger::LOG_FILE, 'a+')
      @@logger = Bullet::BulletLogger.new(@@logger_file)
      @@alert = true
      
      def start_request
        # puts "start request"
      end

      def end_request
        # puts "end request"
        @@object_associations = nil
        @@unpreload_associations = nil
        @@unused_preload_associations = nil
        @@callers = nil
        @@possible_objects = nil
        @@impossible_objects = nil
        @@call_object_associations = nil
      end

      def alert=(alert)
        @@alert = alert
      end

      def logger=(logger)
        if logger == false
          @@logger = nil
        end
      end
      
      def check_unused_preload_associations
        object_associations.each do |object, association|
          call_association = call_object_associations[object] || []
          association.uniq! unless association.flatten!.nil?
          call_association.uniq! unless call_association.flatten!.nil?
          
          add_unused_preload_associations(object.class, association - call_association) unless (association - call_association).empty?
        end
      end
      
      def has_bad_assocations?
        check_unused_preload_associations
        has_unpreload_associations? or has_unused_preload_associations?
      end

      def has_unused_preload_associations?
        !unused_preload_associations.empty?
      end
      
      def has_unpreload_associations?
        !unpreload_associations.empty?
      end

      def bad_associations_alert
        str = ''
        if @@alert
          str = "<script type='text/javascript'>"
          str << "alert('The request has unused preload assocations as follows:\\n"
          str << (has_unused_preload_associations? ? bad_associations_str(unused_preload_associations) : "None")
          str << "\\nThe request has N+1 queries as follows:\\n"
          str << (has_unpreload_associations? ? bad_associations_str(unpreload_associations) : "None")
          str << "')"
          str << "</script>\n"
        end
        str
      end
      
      def bad_associations_str(bad_associations)
        puts bad_associations.inspect
        bad_associations.to_a.collect{|klazz, associations| klazz_associations_str(klazz, associations)}.join('\\n')
      end
      
      def klazz_associations_str(klazz, associations)
        "model: #{klazz} => associations: [#{associations.join(', ')}]"
      end
      
      def associations_str(associations)
        ":include => #{associations.map{|a| a.to_sym unless a.is_a? Hash}.inspect}"
      end

      def log_bad_associations(path)
        if @@logger
          unused_preload_associations.each do |klazz, associations|
            @@logger.info "Unused preload associations: PATH_INFO: #{path};    " + klazz_associations_str(klazz, associations) + "\n Remove from your finder: " + associations_str(associations)
          end
          unpreload_associations.each do |klazz, associations| 
            @@logger.info "N+1 Query: PATH_INFO: #{path};    " + klazz_associations_str(klazz, associations) + "\n Add to your finder: " + associations_str(associations)
          end  
          callers.each do |c|
            @@logger.info "N+1 Query: method call stack: \n" + c.join("\n")
          end
          @@logger_file.flush
        end
      end

      def has_klazz_association(klazz)
        !klazz_associations[klazz].nil? and klazz_associations.keys.include?(klazz)
      end
      
      def define_association(klazz, associations)
        # puts "define association, #{klazz} => #{associations}"
        add_klazz_associations(klazz, associations)
      end

      def call_association(object, associations)
        # puts "call association, #{object} => #{associations}"
        if unpreload_associations?(object, associations)
          add_unpreload_associations(object.class, associations)
          add_call_object_associations(object, associations)
          caller_in_project
        end
      end
      
      def unpreload_associations?(object, associations)
        klazz = object.class
        (!possible_objects[klazz].nil? and possible_objects[klazz].include?(object)) and 
        (impossible_objects[klazz].nil? or !impossible_objects[klazz].include?(object)) and 
        (object_associations[object].nil? or !object_associations[object].include?(associations))
      end

      def add_unpreload_associations(klazz, associations)
        # puts "add unpreload associations, #{klazz} => #{associations.inspect}"
        unpreload_associations[klazz] ||= []
        unpreload_associations[klazz] << associations
        unpreload_associations[klazz].uniq!
      end
      
      def add_unused_preload_associations(klazz, associations)
        # puts "add unused preload associations, #{object} => #{associations.inspect}"
        unused_preload_associations[klazz] ||= []
        unused_preload_associations[klazz] << associations
        unused_preload_associations[klazz].flatten!.uniq!
      end

      def add_association(object, associations)
        # puts "add associations, #{object} => #{associations.inspect}"
        object_associations[object] ||= []
        object_associations[object] << associations
      end

      def add_call_object_associations(object, associations)
        # puts "add call object associations, #{object} => #{associations.inspect}"
        call_object_associations[object] ||= []
        call_object_associations[object] << associations
      end

      def add_possible_objects(objects)
        # puts "add possible objects, #{objects.inspect}"
        klazz= objects.first.class
        possible_objects[klazz] ||= []
        possible_objects[klazz] << objects
        possible_objects[klazz].flatten!.uniq!
      end

      def add_impossible_object(object)
        # puts "add impossible object, #{object}"
        klazz = object.class
        impossible_objects[klazz] ||= []
        impossible_objects[klazz] << object
      end
      
      def add_klazz_associations(klazz, associations)
        # puts "define associations, #{klazz} => #{associations.inspect}"
        klazz_associations[klazz] ||= []
        klazz_associations[klazz] << associations
      end
      
      def unpreload_associations
        @@unpreload_associations ||= {}
      end
      
      def unused_preload_associations
        @@unused_preload_associations ||= {}
      end
      
      def object_associations
        @@object_associations ||= {}
      end
      
      def call_object_associations
        @@call_object_associations ||= {}
      end
      
      def possible_objects
        @@possible_objects ||= {}
      end

      def impossible_objects
        @@impossible_objects ||= {}
      end
      
      def klazz_associations
        @@klazz_associations ||= {}
      end
      
      VENDOR_ROOT = File.join(RAILS_ROOT, 'vendor')
      def caller_in_project
        callers << caller.select {|c| c =~ /#{RAILS_ROOT}/}.reject {|c| c =~ /#{VENDOR_ROOT}/}
        callers.uniq!
      end
      
      def callers
        @@callers ||= []
      end
    end
  end
end