# Mixed into a class that is dynamically created, unless
# the class was created by the Schema.load_schema process
# which builds the whole class, thus no magicalness is 
# needed
module DrNicMagicModels::MagicModel
  def self.append_features(base)
    super
    base.send(:include, InstanceMethods)
    class << base
      # Returns the AssociationReflection object for the named +aggregation+ (use the symbol). Example:
      #   Account.reflect_on_association(:owner) # returns the owner AssociationReflection
      #   Invoice.reflect_on_association(:line_items).macro  # returns :has_many
      def reflect_on_association(association)
        unless reflections[association]
          # See if an assocation can be generated
          self.new.send(association) rescue nil
        end
        reflections[association].is_a?(ActiveRecord::Reflection::AssociationReflection) ? reflections[association] : nil
      end
    end
  end
  
  module InstanceMethods
    
    def method_missing(method, *args, &block)
      begin
        super 
      rescue
        if unknown_method? method
          result = find_belongs_to method, *args, &block 
          result = find_has_some method, *args, &block if not result
          result = find_has_some_indirect method, *args, &block if not result
          return result if result
        end
        add_known_unknown method
        raise
      end
    end
    
    def add_known_unknown(method)
      @known_unknowns ||= {}
      @known_unknowns[method] = true
    end
      
    def unknown_method?(method)
      @known_unknowns.nil? or @known_unknowns.include? method
    end
    
    def find_belongs_to(method, *args, &block)
      method_clean = clean_method method
      fkc = 
        begin
          self.class.connection.foreign_key_constraints(self.class.table_name, method_clean) 
        rescue NotImplementedError 
          nil
        end
      if !fkc.nil? && fkc.length > 0
        foreign_key = fkc.first.foreign_key
        options = {:dependent => :destroy, 
          :foreign_key => fkc.first.foreign_key, 
          :class_name => self.class.class_name(fkc.first.reference_table)}
      else
        foreign_key = self.class.columns.select {|column| column.name == method_clean.to_s.foreign_key}.first
      end
      options ||= {}
      return add_belongs_to(method, method_clean, options, *args, &block) if foreign_key
    end

    def add_belongs_to(method, method_clean, options, *args, &block)
      self.class.send 'belongs_to', method_clean.to_sym, options rescue puts $!
      self.send(method, *args, &block)
    end
    
    def find_has_some(method, *args, &block)
      method_clean = clean_method method
      fkc = [method_clean.to_s.pluralize, method_clean.to_s.singularize].inject({}) do |pair, table_name|
        fkc = begin
                self.class.connection.foreign_key_constraints(table_name)
              rescue NotImplementedError
                nil
              end
        pair[table_name] = fkc if not fkc.blank?
        pair
      end
      if not fkc.blank?
        # assumes there is only one table found - that schema doesn't have a singular and plural table of same name
        foreign_key = fkc.values.first.find {|fk| fk.reference_table == self.class.table_name}
        if foreign_key
          foreign_key = foreign_key.foreign_key
          table_name = fkc.keys.first
          klass = Module.const_get table_name.singularize.camelize rescue nil
          options = {:foreign_key => foreign_key, :class_name => klass.name}
        end
      end
      unless foreign_key
        klass = Module.const_get method_clean.to_s.downcase.singularize.camelize rescue nil
        foreign_key = klass.columns.select {|column| column.name == self.class.name.foreign_key}.first if klass
      end
      options ||= {}
      return add_has_some(method, method_clean, options, *args, &block) if foreign_key
    end
          
    def add_has_some(method, method_clean, options, *args, &block)
      association = method_clean.singularize == method_clean ? 'has_one' : 'has_many'
      self.class.send association, method_clean.to_sym, options rescue puts $!
      self.send(method, *args, &block)
    end
    
    def find_has_some_indirect(method, *args, &block)
      klass = Module.const_get method.to_s.downcase.singularize.camelize rescue return
      join_table = nil
      self.connection.tables.each do |table|
        unless [self.class.table_name, klass.table_name].include? table
          columns = self.connection.columns(table).map(&:name)
          join_table = table if columns.include?(self.class.to_s.foreign_key) and columns.include?(klass.to_s.foreign_key) 
        end
        break if join_table
      end
      return add_has_some_through(join_table, method, *args, &block) if join_table
    end
    
    def add_has_some_through(join_table, method, *args, &block)
      self.class.send 'has_many', method, :through => join_table.to_sym
      self.send(method, *args, &block)
    end
    
  private
    
    def clean_method(method)
      method.to_s.gsub(/=$/,'') # remove any = from the end of the method name
    end
  end
end