# frozen-string-literal: true

require 'sqlite3'
require_relative 'shared/sqlite'

module Sequel
  module SQLite
    FALSE_VALUES = (%w'0 false f no n'.each(&:freeze) + [0]).freeze

    blob = Object.new
    def blob.call(s)
      Sequel::SQL::Blob.new(s.to_s)
    end

    boolean = Object.new
    def boolean.call(s)
      s = s.downcase if s.is_a?(String)
      !FALSE_VALUES.include?(s)
    end

    date = Object.new
    def date.call(s)
      case s
      when String
        Sequel.string_to_date(s)
      when Integer
        Date.jd(s)
      when Float
        Date.jd(s.to_i)
      else
        raise Sequel::Error, "unhandled type when converting to date: #{s.inspect} (#{s.class.inspect})"
      end
    end

    integer = Object.new
    def integer.call(s)
      s.to_i
    end

    float = Object.new
    def float.call(s)
      s.to_f
    end

    numeric = Object.new
    def numeric.call(s)
      s = s.to_s unless s.is_a?(String)
      BigDecimal(s) rescue s
    end

    time = Object.new
    def time.call(s)
      case s
      when String
        Sequel.string_to_time(s)
      when Integer
        Sequel::SQLTime.create(s/3600, (s % 3600)/60, s % 60)
      when Float
        s, f = s.divmod(1)
        Sequel::SQLTime.create(s/3600, (s % 3600)/60, s % 60, (f*1000000).round)
      else
        raise Sequel::Error, "unhandled type when converting to date: #{s.inspect} (#{s.class.inspect})"
      end
    end

    # Hash with string keys and callable values for converting SQLite types.
    SQLITE_TYPES = {}
    {
      %w'date' => date,
      %w'time' => time,
      %w'bit bool boolean' => boolean,
      %w'integer smallint mediumint int bigint' => integer,
      %w'numeric decimal money' => numeric,
      %w'float double real dec fixed' + ['double precision'] => float,
      %w'blob' => blob
    }.each do |k,v|
      k.each{|n| SQLITE_TYPES[n] = v}
    end
    SQLITE_TYPES.freeze

    sqlite_version = SQLite3::VERSION.split('.').map(&:to_i)[0..1]
    sqlite_version = sqlite_version[0] * 100 + sqlite_version[1]
    USE_EXTENDED_RESULT_CODES = sqlite_version >= 104
    
    class Database < Sequel::Database
      include ::Sequel::SQLite::DatabaseMethods
      
      set_adapter_scheme :sqlite
      
      # Mimic the file:// uri, by having 2 preceding slashes specify a relative
      # path, and 3 preceding slashes specify an absolute path.
      # Also support no preceding slashes to specify a relative path.
      def self.uri_to_options(uri) # :nodoc:
        database = if uri.host.nil?
          case uri.path
          when '/'
            nil
          when nil
            uri.opaque
          else
            uri.path
          end
        else
          "#{uri.host}#{uri.path}"
        end

        { :database => database }
      end
      
      private_class_method :uri_to_options

      # The conversion procs to use for this database
      attr_reader :conversion_procs

      def initialize(opts = OPTS)
        super
        @allow_regexp = typecast_value_boolean(opts[:setup_regexp_function])
      end

      # Connect to the database. Since SQLite is a file based database,
      # available options are limited:
      #
      # :database :: database name (filename or ':memory:' or file: URI)
      # :readonly :: open database in read-only mode; useful for reading
      #              static data that you do not want to modify
      # :disable_dqs :: disable double quoted strings in DDL and DML statements
      #                 (requires SQLite 3.29.0+ and sqlite3 gem version 1.4.3+).
      # :timeout :: how long to wait for the database to be available if it
      #             is locked, given in milliseconds (default is 5000)
      # :setup_regexp_function :: enable use of Regexp objects with SQL
      #             'REGEXP' operator. If the value is :cached or "cached",
      #             caches the generated regexps, which can result in a memory
      #             leak if dynamic regexps are used.  If the value is a Proc,
      #             it will be called with a string for the regexp and a string
      #             for the value to compare, and should return whether the regexp
      #             matches.
      # :regexp_function_cache :: If setting +setup_regexp_function+ to +cached+, this
      #             determines the cache to use.  It should either be a proc or a class, and it
      #             defaults to +Hash+. You can use +ObjectSpace::WeakKeyMap+ on Ruby 3.3+ to
      #             have the VM automatically remove regexps from the cache after they
      #             are no longer used.
      def connect(server)
        opts = server_opts(server)
        opts[:database] = ':memory:' if blank_object?(opts[:database])
        sqlite3_opts = {}
        sqlite3_opts[:readonly] = typecast_value_boolean(opts[:readonly]) if opts.has_key?(:readonly)
        # SEQUEL6: Make strict: true the default behavior
        sqlite3_opts[:strict] = typecast_value_boolean(opts[:disable_dqs]) if opts.has_key?(:disable_dqs)
        db = ::SQLite3::Database.new(opts[:database].to_s, sqlite3_opts)
        db.busy_timeout(typecast_value_integer(opts.fetch(:timeout, 5000)))

        if USE_EXTENDED_RESULT_CODES
          db.extended_result_codes = true
        end
        
        connection_pragmas.each{|s| log_connection_yield(s, db){db.execute_batch(s)}}

        if typecast_value_boolean(opts[:setup_regexp_function])
          setup_regexp_function(db, opts[:setup_regexp_function])
        end
        
        class << db
          attr_reader :prepared_statements
        end
        db.instance_variable_set(:@prepared_statements, {})
        
        db
      end

      # Whether this Database instance is setup to allow regexp matching.
      # True if the :setup_regexp_function option was passed when creating the Database.
      def allow_regexp?
        @allow_regexp
      end

      # Disconnect given connections from the database.
      def disconnect_connection(c)
        c.prepared_statements.each_value{|v| v.first.close}
        c.close
      end
      
      # Run the given SQL with the given arguments and yield each row.
      def execute(sql, opts=OPTS, &block)
        _execute(:select, sql, opts, &block)
      end

      # Run the given SQL with the given arguments and return the number of changed rows.
      def execute_dui(sql, opts=OPTS)
        _execute(:update, sql, opts)
      end
      
      # Drop any prepared statements on the connection when executing DDL.  This is because
      # prepared statements lock the table in such a way that you can't drop or alter the
      # table while a prepared statement that references it still exists.
      def execute_ddl(sql, opts=OPTS)
        synchronize(opts[:server]) do |conn|
          conn.prepared_statements.values.each{|cps, s| cps.close}
          conn.prepared_statements.clear
          super
        end
      end
      
      def execute_insert(sql, opts=OPTS)
        _execute(:insert, sql, opts)
      end
      
      def freeze
        @conversion_procs.freeze
        super
      end

      # Handle Integer and Float arguments, since SQLite can store timestamps as integers and floats.
      def to_application_timestamp(s)
        case s
        when String
          super
        when Integer
          super(Time.at(s).to_s)
        when Float
          super(DateTime.jd(s).to_s)
        else
          raise Sequel::Error, "unhandled type when converting to : #{s.inspect} (#{s.class.inspect})"
        end
      end

      private
      
      def adapter_initialize
        @conversion_procs = SQLITE_TYPES.dup
        @conversion_procs['datetime'] = @conversion_procs['timestamp'] = method(:to_application_timestamp)
        set_integer_booleans
      end

      def setup_regexp_function(db, how)
        case how
        when Proc
          # nothing
        when :cached, "cached"
          cache = @opts[:regexp_function_cache] || Hash
          cache = cache.is_a?(Proc) ? cache.call : cache.new
          how = if RUBY_VERSION >= '2.4'
            lambda do |regexp_str, str|
              (cache[regexp_str] ||= Regexp.new(regexp_str)).match?(str)
            end
          else
            lambda do |regexp_str, str|
              (cache[regexp_str] ||= Regexp.new(regexp_str)).match(str)
            end
          end
        else
          how = if RUBY_VERSION >= '2.4'
            lambda{|regexp_str, str| Regexp.new(regexp_str).match?(str)}
          else
            lambda{|regexp_str, str| Regexp.new(regexp_str).match(str)}
          end
        end

        db.create_function("regexp", 2) do |func, regexp_str, str|
          func.result = how.call(regexp_str, str) ? 1 : 0
        end
      end
      
      # Yield an available connection.  Rescue
      # any SQLite3::Exceptions and turn them into DatabaseErrors.
      def _execute(type, sql, opts, &block)
        synchronize(opts[:server]) do |conn|
          return execute_prepared_statement(conn, type, sql, opts, &block) if sql.is_a?(Symbol)
          log_args = opts[:arguments]
          args = {}
          opts.fetch(:arguments, OPTS).each{|k, v| args[k] = prepared_statement_argument(v)}
          case type
          when :select
            log_connection_yield(sql, conn, log_args){conn.query(sql, args, &block)}
          when :insert
            log_connection_yield(sql, conn, log_args){conn.execute(sql, args)}
            conn.last_insert_row_id
          when :update
            log_connection_yield(sql, conn, log_args){conn.execute_batch(sql, args)}
            conn.changes
          end
        end
      rescue SQLite3::Exception => e
        raise_error(e)
      end
      
      # The SQLite adapter does not need the pool to convert exceptions.
      # Also, force the max connections to 1 if a memory database is being
      # used, as otherwise each connection gets a separate database.
      def connection_pool_default_options
        o = super.dup
        # Default to only a single connection if a memory database is used,
        # because otherwise each connection will get a separate database
        o[:max_connections] = 1 if @opts[:database] == ':memory:' || blank_object?(@opts[:database])
        o
      end
      
      def prepared_statement_argument(arg)
        case arg
        when Date, DateTime, Time
          literal(arg)[1...-1]
        when SQL::Blob
          arg.to_blob
        when true, false
          if integer_booleans
            arg ? 1 : 0
          else
            literal(arg)[1...-1]
          end
        else
          arg
        end
      end

      # Execute a prepared statement on the database using the given name.
      def execute_prepared_statement(conn, type, name, opts, &block)
        ps = prepared_statement(name)
        sql = ps.prepared_sql
        args = opts[:arguments]
        ps_args = {}
        args.each{|k, v| ps_args[k] = prepared_statement_argument(v)}
        if cpsa = conn.prepared_statements[name]
          cps, cps_sql = cpsa
          if cps_sql != sql
            cps.close
            cps = nil
          end
        end
        unless cps
          cps = log_connection_yield("PREPARE #{name}: #{sql}", conn){conn.prepare(sql)}
          conn.prepared_statements[name] = [cps, sql]
        end
        log_sql = String.new
        log_sql << "EXECUTE #{name}"
        if ps.log_sql
          log_sql << " ("
          log_sql << sql
          log_sql << ")"
        end
        if block
          log_connection_yield(log_sql, conn, args){cps.execute(ps_args, &block)}
        else
          log_connection_yield(log_sql, conn, args){cps.execute!(ps_args){|r|}}
          case type
          when :insert
            conn.last_insert_row_id
          when :update
            conn.changes
          end
        end
      end
      
      # SQLite3 raises ArgumentError in addition to SQLite3::Exception in
      # some cases, such as operations on a closed database.
      def database_error_classes
        [SQLite3::Exception, ArgumentError]
      end

      def dataset_class_default
        Dataset
      end

      if USE_EXTENDED_RESULT_CODES
        # Support SQLite exception codes if ruby-sqlite3 supports them.
        def sqlite_error_code(exception)
          exception.code if exception.respond_to?(:code)
        end
      end
    end
    
    class Dataset < Sequel::Dataset
      include ::Sequel::SQLite::DatasetMethods

      module ArgumentMapper
        include Sequel::Dataset::ArgumentMapper
        
        protected
        
        # Return a hash with the same values as the given hash,
        # but with the keys converted to strings.
        def map_to_prepared_args(hash)
          args = {}
          hash.each{|k,v| args[k.to_s.gsub('.', '__')] = v}
          args
        end
        
        private
        
        # SQLite uses a : before the name of the argument for named
        # arguments.
        def prepared_arg(k)
          LiteralString.new("#{prepared_arg_placeholder}#{k.to_s.gsub('.', '__')}".freeze)
        end
      end
      
      BindArgumentMethods = prepared_statements_module(:bind, ArgumentMapper)
      PreparedStatementMethods = prepared_statements_module(:prepare, BindArgumentMethods)

      # Support regexp functions if using :setup_regexp_function Database option.
      def complex_expression_sql_append(sql, op, args)
        case op
        when :~, :'!~', :'~*', :'!~*'
          return super unless supports_regexp?

          case_insensitive = [:'~*', :'!~*'].include?(op)
          sql << 'NOT ' if [:'!~', :'!~*'].include?(op)
          sql << '('
          sql << 'LOWER(' if case_insensitive
          literal_append(sql, args[0])
          sql << ')' if case_insensitive
          sql << ' REGEXP '
          sql << 'LOWER(' if case_insensitive
          literal_append(sql, args[1])
          sql << ')' if case_insensitive
          sql << ')'
        else
          super
        end
      end

      def fetch_rows(sql)
        execute(sql) do |result|
          cps = db.conversion_procs
          type_procs = result.types.map{|t| cps[base_type_name(t)]}
          j = -1
          cols = result.columns.map{|c| [output_identifier(c), type_procs[(j+=1)]]}
          self.columns = cols.map(&:first)
          max = cols.length
          result.each do |values|
            row = {}
            i = -1
            while (i += 1) < max
              name, type_proc = cols[i]
              v = values[i]
              if type_proc && v
                v = type_proc.call(v)
              end
              row[name] = v
            end
            yield row
          end
        end
      end

      # Support regexp if using :setup_regexp_function Database option.
      def supports_regexp?
        db.allow_regexp?
      end
      
      private
      
      # The base type name for a given type, without any parenthetical part.
      def base_type_name(t)
        (t =~ /^(.*?)\(/ ? $1 : t).downcase if t
      end

      # Quote the string using the adapter class method.
      def literal_string_append(sql, v)
        sql << "'" << ::SQLite3::Database.quote(v) << "'"
      end

      def bound_variable_modules
        [BindArgumentMethods]
      end

      def prepared_statement_modules
        [PreparedStatementMethods]
      end

      # SQLite uses a : before the name of the argument as a placeholder.
      def prepared_arg_placeholder
        ':'
      end
    end
  end
end
