require 'sqlite3/errors'
require 'sqlite3/resultset'

class String
  def to_blob
    SQLite3::Blob.new( self )
  end
end

module SQLite3

  # A class for differentiating between strings and blobs, when binding them
  # into statements.
  class Blob < String; end

  # A statement represents a prepared-but-unexecuted SQL query. It will rarely
  # (if ever) be instantiated directly by a client, and is most often obtained
  # via the Database#prepare method.
  class Statement

    # This is any text that followed the first valid SQL statement in the text
    # with which the statement was initialized. If there was no trailing text,
    # this will be the empty string.
    attr_reader :remainder

    # The underlying opaque handle used to access the SQLite @driver.
    attr_reader :handle

    # Create a new statement attached to the given Database instance, and which
    # encapsulates the given SQL text. If the text contains more than one
    # statement (i.e., separated by semicolons), then the #remainder property
    # will be set to the trailing text.
    def initialize( db, sql, utf16=false )
      raise ArgumentError, "nil argument passed as sql text" unless sql
      @db = db
      @driver = @db.driver
      @closed = false
      @results = @columns = nil
      result, @handle, @remainder = @driver.prepare( @db.handle, sql )
      Error.check( result, @db )
    end

    # Closes the statement by finalizing the underlying statement
    # handle. The statement must not be used after being closed.
    def close
      must_be_open!
      @closed = true
      @driver.finalize( @handle )
    end

    # Returns true if the underlying statement has been closed.
    def closed?
      @closed
    end

    # Binds the given variables to the corresponding placeholders in the SQL
    # text.
    #
    # See Database#execute for a description of the valid placeholder
    # syntaxes.
    #
    # Example:
    #
    #   stmt = db.prepare( "select * from table where a=? and b=?" )
    #   stmt.bind_params( 15, "hello" )
    #
    # See also #execute, #bind_param, Statement#bind_param, and
    # Statement#bind_params.
    def bind_params( *bind_vars )
      index = 1
      bind_vars.flatten.each do |var|
        if Hash === var
          var.each { |key, val| bind_param key, val }
        else
          bind_param index, var
          index += 1
        end
      end
    end

    # Binds value to the named (or positional) placeholder. If +param+ is a
    # Fixnum, it is treated as an index for a positional placeholder.
    # Otherwise it is used as the name of the placeholder to bind to.
    #
    # See also #bind_params.
    def bind_param( param, value )
      must_be_open!
      reset! if active?
      if Fixnum === param
        case value
          when Bignum then
            @driver.bind_int64( @handle, param, value )
          when Integer then
            if value >= (2 ** 31)
              @driver.bind_int64( @handle, param, value )
            else
              @driver.bind_int( @handle, param, value )
            end
          when Numeric then
            @driver.bind_double( @handle, param, value.to_f )
          when Blob then
            @driver.bind_blob( @handle, param, value )
          when nil then
            @driver.bind_null( @handle, param )
          else
            @driver.bind_text( @handle, param, value )
        end
      else
        param = param.to_s
        param = ":#{param}" unless param[0] == ?:
        index = @driver.bind_parameter_index( @handle, param )
        raise Exception, "no such bind parameter '#{param}'" if index == 0
        bind_param index, value
      end
    end

    # Execute the statement. This creates a new ResultSet object for the
    # statement's virtual machine. If a block was given, the new ResultSet will
    # be yielded to it; otherwise, the ResultSet will be returned.
    #
    # Any parameters will be bound to the statement using #bind_params.
    #
    # Example:
    #
    #   stmt = db.prepare( "select * from table" )
    #   stmt.execute do |result|
    #     ...
    #   end
    #
    # See also #bind_params, #execute!.
    def execute( *bind_vars )
      must_be_open!
      reset! if active?

      bind_params(*bind_vars) unless bind_vars.empty?
      @results = ResultSet.new( @db, self )

      if block_given?
        yield @results
      else
        return @results
      end
    end

    # Execute the statement. If no block was given, this returns an array of
    # rows returned by executing the statement. Otherwise, each row will be
    # yielded to the block.
    #
    # Any parameters will be bound to the statement using #bind_params.
    #
    # Example:
    #
    #   stmt = db.prepare( "select * from table" )
    #   stmt.execute! do |row|
    #     ...
    #   end
    #
    # See also #bind_params, #execute.
    def execute!( *bind_vars )
      result = execute( *bind_vars )
      rows = [] unless block_given?
      while row = result.next
        if block_given?
          yield row
        else
          rows << row
        end
      end
      rows
    end

    # Resets the statement. This is typically done internally, though it might
    # occassionally be necessary to manually reset the statement.
    def reset!(clear_result=true)
      @driver.reset(@handle)
      @results = nil if clear_result
    end

    # Returns true if the statement is currently active, meaning it has an
    # open result set.
    def active?
      not @results.nil?
    end

    # Return an array of the column names for this statement. Note that this
    # may execute the statement in order to obtain the metadata; this makes it
    # a (potentially) expensive operation.
    def columns
      get_metadata unless @columns
      return @columns
    end

    # Return an array of the data types for each column in this statement. Note
    # that this may execute the statement in order to obtain the metadata; this
    # makes it a (potentially) expensive operation.
    def types
      get_metadata unless @types
      return @types
    end

    # A convenience method for obtaining the metadata about the query. Note
    # that this will actually execute the SQL, which means it can be a
    # (potentially) expensive operation.
    def get_metadata
      must_be_open!

      @columns = []
      @types = []

      column_count = @driver.column_count( @handle )
      column_count.times do |column|
        @columns << @driver.column_name( @handle, column )
        @types << @driver.column_decltype( @handle, column )
      end

      @columns.freeze
      @types.freeze
    end
    private :get_metadata

    # Performs a sanity check to ensure that the statement is not
    # closed. If it is, an exception is raised.
    def must_be_open! # :nodoc:
      if @closed
        raise SQLite3::Exception, "cannot use a closed statement"
      end
    end

  end

end
