require "base64"

module Aqua 
  module Store
    module CouchDB
      class Database
        attr_reader :server, :name, :uri 
        attr_accessor :bulk_cache
     
        # Create a CouchDB database representation from a name. Does not actually create a database on couchdb.
        # It does not ensure that the database actually exists either. Just creates a ruby representation
        # of a ruby database interface.
        # 
        # @param [optional String] Name of database. If not provided server namespace will be used as database name. 
        # @param [optional Hash] Options for initialization. Currently the only option is :server which must be either a CouchDB server object or a symbol representing a server stored in the CouchDB module.
        # @return [Database] The initialized object
        # 
        # @api public
        def initialize( name=nil, opts={})
          name = nil if name && name.empty?
          opts = Mash.new( opts ) unless opts.empty?
          @name = name if name
          initialize_server( opts[:server] )
          @uri = "#{server.uri}/#{namespaced( name )}" 
          self.bulk_cache = []
        end
        
        # Initializes the database server with the option provided. If not option is provided the default CouchDB
        # server is used instead.
        #
        # @param [Symbol, Server]
        #   If server_option argument is a Symbol then the CouchDB server stash will be queried for a matching 
        #   server. CouchDB manages the creation of that server in the stash, if not found. 
        #   If server_option argument is a Server object then then it is added directly to the database object.
        #   No management of the server will be done with CouchDB's server stash.
        # @raise [ArgumentError] Raised if a server_option is passed in and if that option is neither a Hash nor a Symbol.
        # @return [Server]
        #
        # @api private
        def initialize_server( server_option ) 
          if server_option
            if server_option.class == Symbol
              @server = CouchDB.server( server_option )
            elsif server_option.class == Aqua::Store::CouchDB::Server
              @server = server_option # WARNING: this won't get stashed in CouchDB for use with other database.
            else
              raise ArgumentError, ":server option must be a symbol identifying a CouchDB server, or a Server object"
            end
          else        
            @server = CouchDB.server
          end 
          @server  
        end
        
        # Namespaces the database path for the given server. If no name is provided, then the database name is
        # just the Server's namespace.
        # 
        # @param [String] Name that the database is initialized with, if any.
        # @return [String] Namespaced database for use as a http path
        # 
        # @api private
        def namespaced( name )
          if name 
            "#{server.namespace}_#{CouchDB.escape(@name)}"
          else
            server.namespace
          end     
        end  
    
        # Creates a database representation and PUTs it on the CouchDB server. 
        # If successfull returns a database object. If not successful in creating
        # the database on the CouchDB server then, false will be returned.
        #
        # @see Aqua::Store::CouchDB#initialize for option details
        # @return [Database, false] Will return the database on success, and false if it did not succeed. 
        #
        # @api pubilc
        def self.create( name=nil, opts={} )
          db = new(name, opts)
          begin
            CouchDB.put( db.uri )
          rescue Exception => e # catch database already exists errors ... 
            unless e.message.match(/412/)
              db = false
            end   
          end
          db    
        end 
        
        # Creates a database representation and PUTs it on the CouchDB server. 
        # This version of the #create method raises an error if the PUT request fails. 
        # The exception on this, is if the database already exists then the 412 HTTP code will be ignored.
        #
        # @see Aqua::Store::CouchDB#initialize for option details
        # @return [Database] Will return the database on success. 
        # @raise HttpAdapter Exceptions depending on the reason for failure.
        #
        # @api pubilc
        def self.create!( name=nil, opts={} ) 
          db = new( name, opts )
          begin
            CouchDB.put( db.uri )
          rescue Exception => e # catch database already exists errors ... 
            raise e unless e.class == RequestFailed && e.message.match(/412/) 
          end 
          db
        end  
    
        # Checks to see if the database exists on the couchdb server.
        #
        # @return [true, false] depending on whether the database already exists in CouchDB land
        #
        # @api public
        def exists?
          begin 
            info 
            true
          rescue CouchDB::ResourceNotFound  
            false
          end  
        end  
        
        # GET the database info from CouchDB
        def info
          CouchDB.get( uri )
        end
     
        # Deletes a database; use with caution as this isn't reversible.
        # 
        # @return A JSON response on success. nil if the resource is not found. And raises an error if another exception was raised
        # @raise Exception related to request failure that is not a ResourceNotFound error.
        def delete
          begin 
            CouchDB.delete( uri )
          rescue CouchDB::ResourceNotFound
            nil
          end    
        end  
        
        # Deletes a database; use with caution as this isn't reversible. Similar to #delete, 
        # except that it will raise an error on failure to find the database.
        # 
        # @return A JSON response on success. 
        # @raise Exception related to request failure or ResourceNotFound.
        def delete!
          CouchDB.delete( uri )
        end  
    
        # # Query the <tt>_all_docs</tt> view. Accepts all the same arguments as view.
        def documents(params = {})
          keys = params.delete(:keys)
          url = CouchDB.paramify_url( "#{uri}/_all_docs", params )
          if keys
            CouchDB.post(url, {:keys => keys})
          else
            CouchDB.get url
          end
        end   
    
        # BULK ACTIVITIES ------------------------------------------
        def add_to_bulk_cache( doc ) 
          if server.uuid_count/2.0 > bulk_cache.size
            self.bulk_cache << doc 
          else
            bulk_save
            self.bulk_cache << doc
          end    
        end
    
        def bulk_save
          docs = bulk_cache
          self.bulk_cache = []
          CouchDB.post( "#{uri}/_bulk_docs", {:docs => docs} )
        end
    
        # # load a set of documents by passing an array of ids
        # def get_bulk(ids)
        #   documents(:keys => ids, :include_docs => true)
        # end
        # alias :bulk_load :get_bulk
        #   
        # # POST a temporary view function to CouchDB for querying. This is not
        # # recommended, as you don't get any performance benefit from CouchDB's
        # # materialized views. Can be quite slow on large databases.
        # def slow_view(funcs, params = {})
        #   keys = params.delete(:keys)
        #   funcs = funcs.merge({:keys => keys}) if keys
        #   url = CouchDB.paramify_url "#{@root}/_temp_view", params
        #   JSON.parse(HttpAbstraction.post(url, funcs.to_json, {"Content-Type" => 'application/json'}))
        # end
        # 
        # # backwards compatibility is a plus
        # alias :temp_view :slow_view
        #   
        # # Query a CouchDB view as defined by a <tt>_design</tt> document. Accepts
        # # paramaters as described in http://wiki.apache.org/couchdb/HttpViewApi
        # def view(name, params = {}, &block)
        #   keys = params.delete(:keys)
        #   name = name.split('/') # I think this will always be length == 2, but maybe not...
        #   dname = name.shift
        #   vname = name.join('/')
        #   url = CouchDB.paramify_url "#{@root}/_design/#{dname}/_view/#{vname}", params
        #   if keys
        #     CouchDB.post(url, {:keys => keys})
        #   else
        #     if block_given?
        #       @streamer.view("_design/#{dname}/_view/#{vname}", params, &block)
        #     else
        #       CouchDB.get url
        #     end
        #   end
        # end
        # 
        # # GET a document from CouchDB, by id. Returns a Ruby Hash.
        # def get(id, params = {})
        #   slug = escape_docid(id)
        #   url = CouchDB.paramify_url("#{@root}/#{slug}", params)
        #   result = CouchDB.get(url)
        #   return result unless result.is_a?(Hash)
        #   doc = if /^_design/ =~ result["_id"]
        #     Design.new(result)
        #   else
        #     Document.new(result)
        #   end
        #   doc.database = self
        #   doc
        # end
        # 
        # # Compact the database, removing old document revisions and optimizing space use.
        # def compact!
        #   CouchDB.post "#{@root}/_compact"
        # end
        # 
        # # Delete and re create the database
        # def recreate!
        #   delete!
        #   create!
        # rescue HttpAbstraction::ResourceNotFound
        # ensure
        #   create!
        # end
        # 
        # # Replicates via "pulling" from another database to this database. Makes no attempt to deal with conflicts.
        # def replicate_from other_db
        #   raise ArgumentError, "must provide a CouchReset::Database" unless other_db.kind_of?(CouchDB::Database)
        #   CouchDB.post "#{@host}/_replicate", :source => other_db.root, :target => name
        # end
        # 
        # # Replicates via "pushing" to another database. Makes no attempt to deal with conflicts.
        # def replicate_to other_db
        #   raise ArgumentError, "must provide a CouchReset::Database" unless other_db.kind_of?(CouchDB::Database)
        #   CouchDB.post "#{@host}/_replicate", :target => other_db.root, :source => name
        # end
        # 
        # 
        # private
        # 
        # def clear_extended_doc_fresh_cache
        #   ::CouchDB::ExtendedDocument.subclasses.each{|klass| klass.design_doc_fresh = false if klass.respond_to?(:design_doc_fresh=) }
        # end
          
      end # Database       
    end # CouchDB
  end # Store  
end # Aqua