# oci8.rb -- OCI8::Cursor
#
# Copyright (C) 2002-2012 KUBO Takehiro <kubo@jiubao.org>
#

#
class OCI8

  # The instance of this class corresponds to cursor in the term of
  # Oracle, which corresponds to java.sql.Statement of JDBC and statement
  # handle $sth of Perl/DBI.
  #
  # Don't create the instance by calling 'new' method. Please create it by
  # calling OCI8#exec or OCI8#parse.
  class Cursor

    # @note Don't use this constructor. Use {OCI8#parse} instead.
    #
    # @param [OCI8] conn   connection
    # @param [String] sql  SQL statement
    def initialize(conn, sql = nil)
      @bind_handles = {}
      @define_handles = []
      @column_metadata = []
      @names = nil
      @con = conn
      @max_array_size = nil
      @fetch_array_size = nil
      @rowbuf_size = 0
      @rowbuf_index = 0
      __initialize(conn, sql) # Initialize the internal C structure.
      self.prefetch_rows = conn.instance_variable_get(:@prefetch_rows)
    end

    # explicitly indicate the date type of fetched value. run this
    # method within parse and exec. pos starts from 1. lentgh is used
    # when type is String.
    # 
    # example:
    #   cursor = conn.parse("SELECT ename, hiredate FROM emp")
    #   cursor.define(1, String, 20) # fetch the first column as String.
    #   cursor.define(2, Time)       # fetch the second column as Time.
    #   cursor.exec()
    def define(pos, type, length = nil)
      bindobj = make_bind_object({:type => type, :length => length}, @fetch_array_size || 1)
      __define(pos, bindobj)
      if old = @define_handles[pos - 1]
        old.send(:free)
      end
      @define_handles[pos - 1] = bindobj
      self
    end

    # Binds variables explicitly.
    # 
    # When key is number, it binds by position, which starts from 1.
    # When key is string, it binds by the name of placeholder.
    #
    # example:
    #   cursor = conn.parse("SELECT * FROM emp WHERE ename = :ename")
    #   cursor.bind_param(1, 'SMITH') # bind by position
    #   # ...or...
    #   cursor.bind_param(':ename', 'SMITH') # bind by name
    #
    # To bind as number, set the number intself to +val+. If its initial value
    # is NULL, please set nil to +type+ and Integer, Float or OraNumber to +val+.
    #
    # example:
    #   cursor.bind_param(1, 1234) # bind as Integer, Initial value is 1234.
    #   cursor.bind_param(1, 1234.0) # bind as Float, Initial value is 1234.0.
    #   cursor.bind_param(1, nil, Integer) # bind as Integer, Initial value is NULL.
    #   cursor.bind_param(1, nil, Float) # bind as Float, Initial value is NULL.
    #   cursor.bind_param(1, OraNumber(1234)) # bind as OraNumber, Initial value is 1234.
    #   cursor.bind_param(1, nil, OraNumber) # bind as OraNumber, Initial value is NULL.
    #
    # In case of binding a string, set the string itself to
    # +val+. When the bind variable is used as output, set the
    # string whose length is enough to store or set the length.
    #
    # example:
    #   cursor = conn.parse("BEGIN :out := :in || '_OUT'; END;")
    #   cursor.bind_param(':in', 'DATA') # bind as String with width 4.
    #   cursor.bind_param(':out', nil, String, 7) # bind as String with width 7.
    #   cursor.exec()
    #   p cursor[':out'] # => 'DATA_OU'
    #   # Though the length of :out is 8 bytes in PL/SQL block, it is
    #   # bound as 7 bytes. So result is cut off at 7 byte.
    #
    # In case of binding a string as RAW, set OCI::RAW to +type+.
    #
    # example:
    #   cursor = conn.parse("INSERT INTO raw_table(raw_column) VALUE (:1)")
    #   cursor.bind_param(1, 'RAW_STRING', OCI8::RAW)
    #   cursor.exec()
    #   cursor.close()
    def bind_param(key, param, type = nil, length = nil)
      case param
      when Hash
      when Class
        param = {:value => nil,   :type => param, :length => length}
      else
        param = {:value => param, :type => type,  :length => length}
      end
      bindobj = make_bind_object(param)
      __bind(key, bindobj)
      if old = @bind_handles[key]
        old.send(:free)
      end
      @bind_handles[key] = bindobj
      self
    end

    # Executes the SQL statement assigned the cursor. The type of
    # return value depends on the type of sql statement: select;
    # insert, update and delete; create, alter, drop and PL/SQL.
    #
    # In case of select statement, it returns the number of the
    # select-list.
    #
    # In case of insert, update or delete statement, it returns the
    # number of processed rows.
    #
    # In case of create, alter, drop and PL/SQL statement, it returns
    # true. In contrast with OCI8#exec, it returns true even
    # though PL/SQL. Use OCI8::Cursor#[] explicitly to get bind
    # variables.
    def exec(*bindvars)
      bind_params(*bindvars)
      case type
      when :select_stmt
        __execute(0)
        define_columns() if @column_metadata.size == 0
        @rowbuf_size = 0
        @rowbuf_index = 0
        @column_metadata.size
      else
        __execute(1)
        row_count
      end
    end

    # Gets fetched data as array. This is available for select
    # statement only.
    #
    # @example
    #   conn = OCI8.new('scott', 'tiger')
    #   cursor = conn.exec('SELECT * FROM emp')
    #   while r = cursor.fetch()
    #     puts r.join(',')
    #   end
    #   cursor.close
    #   conn.logoff
    #
    # @return [Array]
    def fetch
      if block_given?
        while row = fetch_one_row_as_array
          yield row
        end
        self
      else
        fetch_one_row_as_array
      end
    end

    # Gets fetched data as a Hash. The hash keys are column names.
    # If a block is given, acts as an iterator.
    #
    # @return [Hash] the hash keys are column names and hash values are column values
    def fetch_hash
      if block_given?
        while row = fetch_one_row_as_hash()
          yield row
        end
      else
        fetch_one_row_as_hash
      end
    end

    # Gets the value of the bind variable.
    #
    # When bind variables are explicitly bound by {OCI8::Cursor#bind_param},
    # the subscript +key+ must be same with the parameter +key+ passed to {OCI8::Cursor#bind_param}.
    #
    # When they are implicitly bound by {OCI8#exec} or {OCI8::Cursor#exec},
    # the subscript +key+ is the position which starts from one.
    #
    # @example explicitly bind by name
    #   cursor = conn.parse("BEGIN :out := 'BAR'; END;")
    #   cursor.bind_param(:out, 'FOO') # bind by name
    #   p cursor[:out] # => 'FOO'  - The subscript must be :out.
    #   cursor.exec()
    #   p cursor[:out] # => 'BAR'
    #
    # @example explicitly bind by position
    #   cursor = conn.parse("BEGIN :out := 'BAR'; END;")
    #   cursor.bind_param(1, 'FOO') # bind by position
    #   p cursor[1] # => 'FOO'  - The subscript must be 1.
    #   cursor.exec()
    #   p cursor[1] # => 'BAR'
    #
    # @example implicitly bind
    #   cursor = conn.exec("BEGIN :out := 'BAR'; END;", 'FOO')
    #   # 1st bind variable is bound as String with width 3. Its initial value is 'FOO'
    #   # After execute, the value become 'BAR'.
    #   p cursor[1] # => 'BAR'
    #
    # @param [Object] key bind key
    # @return [Object] the value of the bind variable
    #
    def [](key)
      handle = @bind_handles[key]
      handle && handle.send(:get_data)
    end

    # Changes the bind variable value.
    #
    # When bind variables are explicitly bound by {OCI8::Cursor#bind_param},
    # the subscript +key+ must be same with the parameter +key+ passed to {OCI8::Cursor#bind_param}.
    #
    # When they are implicitly bound by {OCI8#exec} or {OCI8::Cursor#exec},
    # the subscript +key+ is the position which starts from one.
    #
    # @example
    #   # Inserts three rows whose values are 'FOO', 'BAR' and 'BAZ.'
    #   cursor = conn.parse("INSERT INTO test(col1) VALUES(:1)")
    #   begin
    #     cursor.bind_params(1, nil, String, 3)
    #     ['FOO', 'BAR', 'BAZ'].each do |column_value|
    #       cursor[1] = column_value  # Change the bind value
    #       cursor.exec               # and insert it.
    #     end
    #   ensure
    #     cursor.close()
    #   end
    #   # This makes same result with the following but is more efficient.
    #   #
    #   #  ['FOO', 'BAR', 'BAZ'].each do |column_value|
    #   #    conn.exec("INSERT INTO test(col1) VALUES(:1)", column_value)
    #   #  end
    #   #
    #
    # @param [Object] key bind key
    # @param [Object] val bind value
    #
    def []=(key, val)
      handle = @bind_handles[key]
      return nil if handle.nil?

      if val.is_a? Array
        if @actual_array_size > 0 && val.length != @actual_array_size
          raise RuntimeError, "all binding arrays hould be the same size"
        end
        if @actual_array_size == 0 && val.length <= @max_array_size
          @actual_array_size = val.length
        end
      end
      handle.send(:set_data, val)
      val
    end

    # Set the maximum array size for bind_param_array
    #
    # All the binds will be clean from cursor if instance variable max_array_size is set before
    #
    # Instance variable actual_array_size holds the size of the arrays users actually binds through bind_param_array
    #  all the binding arrays are required to be the same size
    def max_array_size=(size)
      raise "expect positive number for max_array_size." if size.nil? && size <=0
      free_bind_handles()  if !@max_array_size.nil?
      @max_array_size = size
      @actual_array_size = nil
    end

    # Binds array explicitly
    #
    # When key is number, it binds by position, which starts from 1.
    # When key is string, it binds by the name of placeholder.
    # 
    # The max_array_size should be set before calling bind_param_array
    #
    # @example
    #   cursor = conn.parse("INSERT INTO test_table VALUES (:str)")
    #   cursor.max_array_size = 3
    #   cursor.bind_param_array(1, ['happy', 'new', 'year'], String, 30)
    #   cursor.exec_array
    def bind_param_array(key, var_array, type = nil, max_item_length = nil)
      raise "please call max_array_size= first." if @max_array_size.nil?
      raise "expect array as input param for bind_param_array." if !var_array.nil? && !(var_array.is_a? Array) 
      raise "the size of var_array should not be greater than max_array_size." if !var_array.nil? && var_array.size > @max_array_size

      if var_array.nil? 
        raise "all binding arrays should be the same size." unless @actual_array_size.nil? || @actual_array_size == 0
        @actual_array_size = 0
      else
        raise "all binding arrays should be the same size." unless @actual_array_size.nil? || var_array.size == @actual_array_size
        @actual_array_size = var_array.size if @actual_array_size.nil?
      end
      
      param = {:value => var_array, :type => type, :length => max_item_length, :max_array_size => @max_array_size}
      first_non_nil_elem = var_array.nil? ? nil : var_array.find{|x| x!= nil}
      
      if type.nil?
        if first_non_nil_elem.nil?
          raise "bind type is not given."
        else
          type = first_non_nil_elem.class
        end
      end
      
      bindclass = OCI8::BindType::Mapping[type]
      if bindclass.nil? and type.is_a? Class
        bindclass = OCI8::BindType::Mapping[type.to_s]
        OCI8::BindType::Mapping[type] = bindclass if bindclass
      end
      raise "unsupported dataType: #{type}" if bindclass.nil?
      bindobj = bindclass.create(@con, var_array, param, @max_array_size)
      __bind(key, bindobj)
      #
      if old = @bind_handles[key]
        old.send(:free)
      end
      @bind_handles[key] = bindobj
      self
    end

    # Executes the SQL statement assigned the cursor with array binding
    def exec_array
      raise "please call max_array_size= first." if @max_array_size.nil?

      if !@actual_array_size.nil? && @actual_array_size > 0
        __execute(@actual_array_size)
      else
        raise "please set non-nil values to array binding parameters"
      end

      case type
      when :update_stmt, :delete_stmt, :insert_stmt
        row_count
      else
        true
      end
    end

    # Gets the names of select-list as array. Please use this
    # method after exec.
    def get_col_names
      @names ||= @column_metadata.collect { |md| md.name }
    end

    # Gets an array of OCI8::Metadata::Column of a select statement.
    #
    # @example
    #   cursor = conn.exec('select * from tab')
    #   puts ' Name                                      Type'
    #   puts ' ----------------------------------------- ----------------------------'
    #   cursor.column_metadata.each do |colinfo|
    #     puts format(' %-41s %s',
    #                 colinfo.name,
    #                 colinfo.type_string)
    #   end
    #
    # @return [Array of OCI8::Metadata::Column]
    #
    # @since 1.0.0
    def column_metadata
      @column_metadata
    end

    # close the cursor.
    def close
      free()
      @names = nil
      @column_metadata = nil
    end

    # Returns the keys of bind variables.
    #
    # @return [Array] bind variable keys
    def keys
      @bind_handles.keys
    end

    # Set the number of rows to be prefetched.
    # This can reduce the number of network round trips when fetching
    # many rows. The default value is one.
    #
    # FYI: Rails oracle adaptor uses 100 by default.
    #
    # @param [Integer] rows The number of rows to be prefetched
    def prefetch_rows=(rows)
      attr_set_ub4(11, rows) # OCI_ATTR_PREFETCH_ROWS(11)
      @prefetch_rows = rows
    end

    if OCI8::oracle_client_version >= ORAVER_12_1
      # Returns the number of processed rows.
      #
      # @return [Integer]
      def row_count
        # https://docs.oracle.com/database/121/LNOCI/ociaahan.htm#sthref5774
        attr_get_ub8(457) # OCI_ATTR_UB8_ROW_COUNT(457)
      end
    else
      def row_count
        # http://docs.oracle.com/cd/E11882_01/appdev.112/e10646/ociaahan.htm#sthref5498
        attr_get_ub4(9) # OCI_ATTR_ROW_COUNT(9)
      end
    end

    # Returns the text of the SQL statement prepared in the cursor.
    #
    # @note
    #   When {http://docs.oracle.com/cd/E11882_01/server.112/e10729/ch7progrunicode.htm#CACHHIFE
    #   NCHAR String Literal Replacement} is turned on, it returns the modified SQL text,
    #   instead of the original SQL text.
    #
    # @example
    #    cursor = conn.parse("select * from country where country_code = 'ja'")
    #    cursor.statement # => "select * from country where country_code = 'ja'"
    #
    # @return [String] prepared SQL statement
    #
    # @since 2.1.3
    #
    def statement
      # The magic number 144 is OCI_ATTR_STATEMENT.
      # See http://docs.oracle.com/cd/E11882_01/appdev.112/e10646/ociaahan.htm#sthref5503
      attr_get_string(144)
    end

    # gets the type of SQL statement as follows.
    # * OCI8::STMT_SELECT
    # * OCI8::STMT_UPDATE
    # * OCI8::STMT_DELETE
    # * OCI8::STMT_INSERT
    # * OCI8::STMT_CREATE
    # * OCI8::STMT_DROP
    # * OCI8::STMT_ALTER
    # * OCI8::STMT_BEGIN (PL/SQL block which starts with a BEGIN keyword)
    # * OCI8::STMT_DECLARE (PL/SQL block which starts with a DECLARE keyword)
    # * Other Integer value undocumented in Oracle manuals.
    #
    # <em>Changes between ruby-oci8 1.0 and 2.0.</em>
    #
    # [ruby-oci8 2.0] OCI8::STMT_* are Symbols. (:select_stmt, :update_stmt, etc.)
    # [ruby-oci8 1.0] OCI8::STMT_* are Integers. (1, 2, 3, etc.)
    #
    def type
      # http://docs.oracle.com/cd/E11882_01/appdev.112/e10646/ociaahan.htm#sthref5506
      stmt_type = attr_get_ub2(24) # OCI_ATTR_STMT_TYPE(24)
      case stmt_type
      when 1 # OCI_STMT_SELECT
        :select_stmt
      when 2 # OCI_STMT_UPDATE
        :update_stmt
      when 3 # OCI_STMT_DELETE
        :delete_stmt
      when 4 # OCI_STMT_INSERT
        :insert_stmt
      when 5 # OCI_STMT_CREATE
        :create_stmt
      when 6 # OCI_STMT_DROP
        :drop_stmt
      when 7 # OCI_STMT_ALTER
        :alter_stmt
      when 8 # OCI_STMT_BEGIN
        :begin_stmt
      when 9 # OCI_STMT_DECLARE
        :declare_stmt
      else
        stmt_type
      end
    end

    private

    def make_bind_object(param, fetch_array_size = nil)
      case param
      when Hash
        key = param[:type]
        val = param[:value]
        max_array_size = param[:max_array_size]

        if key.nil?
          if val.nil?
            raise "bind type is not given."
          elsif val.is_a? OCI8::Object::Base
            key = :named_type
            param = @con.get_tdo_by_class(val.class)
          else
            key = val.class
          end
        elsif key.class == Class && key < OCI8::Object::Base
          param = @con.get_tdo_by_class(key)
          key = :named_type
        elsif key == :named_type
          param = @con.get_tdo_by_typename(param[:length])
        end
      when OCI8::Metadata::Base
        key = param.data_type
        case key
        when :named_type
          if param.type_name == 'XMLTYPE'
            key = :xmltype
          else
            param = @con.get_tdo_by_metadata(param.type_metadata)
          end
        end
      else
        raise "unknown param #{param.intern}"
      end

      bindclass = OCI8::BindType::Mapping[key]
      if bindclass.nil? and key.is_a? Class
        bindclass = OCI8::BindType::Mapping[key.to_s]
        OCI8::BindType::Mapping[key] = bindclass if bindclass
      end
      raise "unsupported datatype: #{key}" if bindclass.nil?
      bindclass.create(@con, val, param, fetch_array_size || max_array_size)
    end

    @@use_array_fetch = false

    def define_columns
      # http://docs.oracle.com/cd/E11882_01/appdev.112/e10646/ociaahan.htm#sthref5494
      num_cols = attr_get_ub4(18) # OCI_ATTR_PARAM_COUNT(18)
      @column_metadata = 1.upto(num_cols).collect do |i|
        __paramGet(i)
      end
      if @define_handles.size == 0
        use_array_fetch = @@use_array_fetch
        @column_metadata.each do |md|
          case md.data_type
          when :clob, :blob, :bfile
            # Rows prefetching doesn't work for CLOB, BLOB and BFILE.
            # Use array fetching to get more than one row in a network round trip.
            use_array_fetch = true
          end
        end
        @fetch_array_size = @prefetch_rows if use_array_fetch
      end
      @column_metadata.each_with_index do |md, i|
        define_one_column(i + 1, md) unless @define_handles[i]
      end
      num_cols
    end

    def define_one_column(pos, param)
      bindobj = make_bind_object(param, @fetch_array_size || 1)
      __define(pos, bindobj)
      @define_handles[pos - 1] = bindobj
    end

    def bind_params(*bindvars)
      bindvars.each_with_index do |val, i|
        if val.is_a? Array
          bind_param(i + 1, val[0], val[1], val[2])
        else
          bind_param(i + 1, val)
        end
      end
    end

    def fetch_row_internal
      if @rowbuf_size && @rowbuf_size == @rowbuf_index
        @rowbuf_size = __fetch(@con, @fetch_array_size || 1)
        @rowbuf_index = 0
      end
      @rowbuf_size
    end

    def fetch_one_row_as_array
      if fetch_row_internal
        ret = @define_handles.collect do |handle|
          handle.send(:get_data, @rowbuf_index)
        end
        @rowbuf_index += 1
        ret
      else
        nil
      end
    end

    def fetch_one_row_as_hash
      if fetch_row_internal
        ret = {}
        get_col_names.each_with_index do |name, idx|
          ret[name] = @define_handles[idx].send(:get_data, @rowbuf_index)
        end
        @rowbuf_index += 1
        ret
      else
        nil
      end
    end

    def free_bind_handles
      @bind_handles.each_value do |val|
        val.send(:free)
      end
      @bind_handles.clear
    end
  end
end
