require 'uri'

require File.join(File.dirname(__FILE__), 'schema')
require File.join(File.dirname(__FILE__), 'dataset')

module Sequel
  # A Database object represents a virtual connection to a database.
  # The Database class is meant to be subclassed by database adapters in order
  # to provide the functionality needed for executing queries.
  class Database
    attr_reader :opts, :pool
    
    # Constructs a new instance of a database connection with the specified
    # options hash.
    #
    # Sequel::Database is an abstract class that is not useful by itself.
    def initialize(opts = {})
      @opts = opts
      @pool = ConnectionPool.new(@opts[:max_connections] || 4)
      @logger = opts[:logger]
    end
    
    # Returns a blank dataset
    def dataset
      Dataset.new(self)
    end

    # Returns a new dataset with the from method invoked.
    def from(*args); dataset.from(*args); end
    
    # Returns a new dataset with the select method invoked.
    def select(*args); dataset.select(*args); end
    
    alias_method :[], :from

    def execute(sql)
      raise RuntimeError
    end
    
    def synchronize(&block)
      @pool.hold(&block)
    end
      
    def test_connection
      @pool.hold {|conn|} if @pool
      true
    end
    
    # call-seq:
    #   db.execute(sql)
    #   db << sql
    #
    # Executes an sql query.
    def <<(sql)
      execute(sql)
    end

    # Returns a literal SQL representation of a value. This method is usually
    # overriden in database adapters.
    def literal(v)
      case v
      when String: "'%s'" % v
      else v.to_s
      end
    end
    
    # Creates a table. The easiest way to use this method is to provide a
    # block:
    #   DB.create_table :posts do
    #     primary_key :id, :serial
    #     column :title, :text
    #     column :content, :text
    #     index :title
    #   end
    def create_table(name, columns = nil, indexes = nil, &block)
      if block
        schema = Schema.new
        schema.create_table(name, &block)
        schema.create(self)
      else
        execute Schema.create_table_sql(name, columns, indexes)
      end
    end
    
    # Drops a table.
    def drop_table(*names)
      transaction do
        names.each {|n| execute Schema.drop_table_sql(n)}
      end
    end
    
    # Performs a brute-force check for the existance of a table. This method is
    # usually overriden in descendants.
    def table_exists?(name)
      if respond_to?(:tables)
        tables.include?(name.to_sym)
      else
        from(name).first && true
      end
    rescue
      false
    end
    
    SQL_BEGIN = 'BEGIN'.freeze
    SQL_COMMIT = 'COMMIT'.freeze
    SQL_ROLLBACK = 'ROLLBACK'.freeze
    
    def transaction
      @pool.hold do |conn|
        @transactions ||= []
        if @transactions.include? Thread.current
          return yield(conn)
        end
        # ServerSide.info('BEGIN')
        conn.execute(SQL_BEGIN)
        begin
          @transactions << Thread.current
          result = yield(conn)
          # ServerSide.info('COMMIT')
          conn.execute(SQL_COMMIT)
          result
        rescue => e
          # ServerSide.info('ROLLBACK')
          conn.execute(SQL_ROLLBACK)
          raise e
        ensure
          @transactions.delete(Thread.current)
        end
      end
    end
    
    @@adapters = Hash.new
    
    # Sets the adapter scheme for the database class. Call this method in
    # descendnants of Database to allow connection using a URL. For example:
    #   class DB2::Database < Sequel::Database
    #     set_adapter_scheme :db2
    #     ...
    #   end
    def self.set_adapter_scheme(scheme)
      @@adapters[scheme.to_sym] = self
    end
    
    # Converts a uri to an options hash. These options are then passed
    # to a newly created database object.
    def self.uri_to_options(uri)
      {
        :user => uri.user,
        :password => uri.password,
        :host => uri.host,
        :port => uri.port,
        :database => (uri.path =~ /\/(.*)/) && ($1)
      }
    end
    
    # call-seq:
    #   Sequel::Database.connect(conn_string)
    #   Sequel.connect(conn_string)
    #
    # Creates a new database object based on the supplied connection string.
    # The specified scheme determines the database class used, and the rest
    # of the string specifies the connection options. For example:
    #   DB = Sequel.connect('sqlite:///blog.db')
    def self.connect(conn_string)
      uri = URI.parse(conn_string)
      c = @@adapters[uri.scheme.to_sym]
      raise "Invalid database scheme" unless c
      c.new(c.uri_to_options(uri))
    end
  end
end