module DataMapper
  module Adapters
    # Specific adapters extend this class and implement
    # methods for creating, reading, updating and deleting records.
    #
    # Adapters may only implement method for reading or (less common case)
    # writing. Read only adapter may be useful when one needs to work
    # with legacy data that should not be changed or web services that
    # only provide read access to data (from Wordnet and Medline to
    # Atom and RSS syndication feeds)
    #
    # Note that in case of adapters to relational databases it makes
    # sense to inherit from DataObjectsAdapter class.
    class AbstractAdapter
      include DataMapper::Assertions
      extend DataMapper::Assertions
      extend Equalizer

      equalize :name, :options, :resource_naming_convention, :field_naming_convention

      # @api semipublic
      def self.descendants
        @descendants ||= DescendantSet.new
      end

      # @api private
      def self.inherited(descendant)
        descendants << descendant
        super
      end

      # Adapter name
      #
      # @example
      #   adapter.name  # => :default
      #
      # Note that when you use
      #
      # DataMapper.setup(:default, 'postgres://postgres@127.0.0.1/dm_core_test')
      #
      # the adapter name is currently set to :default
      #
      # @return [Symbol]
      #   the adapter name
      #
      # @api semipublic
      attr_reader :name

      # Options with which adapter was set up
      #
      # @example
      #   adapter.options  # => { :adapter => 'yaml', :path => '/tmp' }
      #
      # @return [Hash]
      #   adapter configuration options
      #
      # @api semipublic
      attr_reader :options

      # A callable object returning a naming convention for model storage
      #
      # @example
      #   adapter.resource_naming_convention  # => Proc for model storage name
      #
      # @return [#call]
      #   object to return the naming convention for each model
      #
      # @api semipublic
      attr_accessor :resource_naming_convention

      # A callable object returning a naming convention for property fields
      #
      # @example
      #   adapter.field_naming_convention  # => Proc for field name
      #
      # @return [#call]
      #   object to return the naming convention for each field
      #
      # @api semipublic
      attr_accessor :field_naming_convention

      # Persists one or many new resources
      #
      # @example
      #   adapter.create(collection)  # => 1
      #
      # Adapters provide specific implementation of this method
      #
      # @param [Enumerable<Resource>] resources
      #   The list of resources (model instances) to create
      #
      # @return [Integer]
      #   The number of records that were actually saved into the data-store
      #
      # @api semipublic
      def create(resources)
        raise NotImplementedError, "#{self.class}#create not implemented"
      end

      # Reads one or many resources from a datastore
      #
      # @example
      #   adapter.read(query)  # => [ { 'name' => 'Dan Kubb' } ]
      #
      # Adapters provide specific implementation of this method
      #
      # @param [Query] query
      #   the query to match resources in the datastore
      #
      # @return [Enumerable<Hash>]
      #   an array of hashes to become resources
      #
      # @api semipublic
      def read(query)
        raise NotImplementedError, "#{self.class}#read not implemented"
      end

      # Updates one or many existing resources
      #
      # @example
      #   adapter.update(attributes, collection)  # => 1
      #
      # Adapters provide specific implementation of this method
      #
      # @param [Hash(Property => Object)] attributes
      #   hash of attribute values to set, keyed by Property
      # @param [Collection] collection
      #   collection of records to be updated
      #
      # @return [Integer]
      #   the number of records updated
      #
      # @api semipublic
      def update(attributes, collection)
        raise NotImplementedError, "#{self.class}#update not implemented"
      end

      # Deletes one or many existing resources
      #
      # @example
      #   adapter.delete(collection)  # => 1
      #
      # Adapters provide specific implementation of this method
      #
      # @param [Collection] collection
      #   collection of records to be deleted
      #
      # @return [Integer]
      #   the number of records deleted
      #
      # @api semipublic
      def delete(collection)
        raise NotImplementedError, "#{self.class}#delete not implemented"
      end

      # Create a Query object or subclass.
      #
      # Alter this method if you'd like to return an adapter specific Query subclass.
      #
      # @param [Repository] repository
      #   the Repository to retrieve results from
      # @param [Model] model
      #   the Model to retrieve results from
      # @param [Hash] options
      #   the conditions and scope
      #
      # @return [Query]
      #
      # @api semipublic
      #--
      # TODO: DataObjects::Connection.create_command style magic (Adapter)::Query?
      def new_query(repository, model, options = {})
        Query.new(repository, model, options)
      end

      protected

      # Set the serial value of the Resource
      #
      # @param [Resource] resource
      #   the resource to set the serial property in
      # @param [Integer] next_id
      #   the identifier to set in the resource
      #
      # @return [undefined]
      #
      # @api semipublic
      def initialize_serial(resource, next_id)
        return unless serial = resource.model.serial(name)
        return unless serial.get!(resource).nil?
        serial.set!(resource, next_id)

        # TODO: replace above with this, once
        # specs can handle random, non-sequential ids
        #serial.set!(resource, rand(2**32))
      end

      # Translate the attributes into a Hash with the field as the key
      #
      # @example
      #   attributes = { User.properties[:name] => 'Dan Kubb' }
      #   adapter.attributes_as_fields(attributes)  # => { 'name' => 'Dan Kubb' }
      #
      # @param [Hash] attributes
      #   the attributes with the Property as the key
      #
      # @return [Hash]
      #   the attributes with the Property#field as the key
      #
      # @api semipublic
      def attributes_as_fields(attributes)
        Hash[ attributes.map { |property, value| [ property.field, property.dump(value) ] } ]
      end

      private

      # Initialize an AbstractAdapter instance
      #
      # @param [Symbol] name
      #   the adapter repository name
      # @param [Hash] options
      #   the adapter configuration options
      #
      # @return [undefined]
      #
      # @api semipublic
      def initialize(name, options)
        @name                       = name
        @options                    = options.dup.freeze
        @resource_naming_convention = NamingConventions::Resource::UnderscoredAndPluralized
        @field_naming_convention    = NamingConventions::Field::Underscored
      end
    end # class AbstractAdapter

    const_added(:AbstractAdapter)
  end # module Adapters
end # module DataMapper
