module Pickle
  # Abstract Factory adapter class, if you have a factory type setup, you
  # can easily create an adaptor to make it work with Pickle.
  #
  # The factory adaptor must have a #factories class method that returns 
  # its instances, and each instance must respond to:
  #
  #   #name : identifies the factory by name (default is attr_reader)
  #   #klass : returns the associated model class for this factory (default is attr_reader)
  #   #create(attrs = {}) : returns a newly created object
  class Adapter
    attr_reader :name, :klass
    
    def self.factories
      raise NotImplementedError, "return an array of factory adapter objects"
    end
  
    def create(attrs = {})
      raise NotImplementedError, "create and return an object with the given attributes"
    end
  
    cattr_writer :model_classes
    self.model_classes = nil
    
    def self.model_classes
      # remove abstract, framework, and non-table classes
      @@model_classes ||= ::ActiveRecord::Base.send(:subclasses).reject do |klass|
        klass.abstract_class? || !klass.table_exists? ||
         (defined?(CGI::Session::ActiveRecordStore::Session) && klass == CGI::Session::ActiveRecordStore::Session) ||
         (defined?(::ActiveRecord::SessionStore::Session) && klass == ::ActiveRecord::SessionStore::Session)
      end if class_exists?(::ActiveRecord)
      @@model_classes ||= []
    end

    def self.class_exists?(klass)
      defined?(klass)
    end
  
    # machinist adapter
    class Machinist < Adapter
      def self.factories
        factories = []
        model_classes.each do |klass|
          if blueprints = klass.instance_variable_get('@blueprints')
            blueprints.keys.each {|blueprint| factories << new(klass, blueprint)}
          end
        end
        factories
      end
      
      def initialize(klass, blueprint)
        @klass, @blueprint = klass, blueprint
        @name = @klass.name.underscore.gsub('/','_')
        @name = "#{@blueprint}_#{@name}" unless @blueprint == :master
      end
      
      def create(attrs = {})
        @klass.send(:make, @blueprint, attrs)
      end
    end
    
    # factory-girl adapter
    class FactoryGirl < Adapter
      def self.factories
        (::Factory.factories.values rescue []).map {|factory| new(factory)}
      end
      
      def initialize(factory)
        @klass, @name = factory.build_class, factory.factory_name.to_s
      end
    
      def create(attrs = {})
        Factory.create(@name, attrs)
      end
    end
        
    # fallback active record adapter
    class ActiveRecord < Adapter
      def self.factories
        model_classes.map {|klass| new(klass) }
      end

      def initialize(klass)
        @klass, @name = klass, klass.name.underscore.gsub('/','_')
      end

      def create(attrs = {})
        @klass.send(:create!, attrs)
      end
    end
  end
end