require 'date'
require 'time'

module DuckDB
  if defined?(DuckDB::Appender)
    # The DuckDB::Appender encapsulates DuckDB Appender.
    #
    #   require 'duckdb'
    #   db = DuckDB::Database.open
    #   con = db.connect
    #   con.query('CREATE TABLE users (id INTEGER, name VARCHAR)')
    #   appender = con.appender('users')
    #   appender.append_row(1, 'Alice')
    #
    class Appender
      RANGE_INT16 = -32_768..32_767
      RANGE_INT32 = -2_147_483_648..2_147_483_647
      RANGE_INT64 = -9_223_372_036_854_775_808..9_223_372_036_854_775_807

      #
      # appends huge int value.
      #
      #   require 'duckdb'
      #   db = DuckDB::Database.open
      #   con = db.connect
      #   con.query('CREATE TABLE numbers (num HUGEINT)')
      #   appender = con.appender('numbers')
      #   appender
      #     .begin_row
      #     .append_hugeint(-170_141_183_460_469_231_731_687_303_715_884_105_727)
      #     .end_row
      #
      def append_hugeint(value)
        case value
        when Integer
          if respond_to?(:_append_hugeint, true)
            half = 1 << 64
            upper = value / half
            lower = value - upper * half
            _append_hugeint(lower, upper)
          else
            append_varchar(value.to_s)
          end
        else
          raise(ArgumentError, "2nd argument `#{value}` must be Integer.")
        end
      end

      #
      # appends date value.
      #
      #   require 'duckdb'
      #   db = DuckDB::Database.open
      #   con = db.connect
      #   con.query('CREATE TABLE dates (date_value DATE)')
      #   appender = con.appender('dates')
      #   appender.begin_row
      #   appender.append_date(Date.today)
      #   # or
      #   # appender.append_date(Time.now)
      #   # appender.append_date('2021-10-10')
      #   appender.end_row
      #   appender.flush
      #
      def append_date(value)
        date = case value
               when Date, Time
                 value
               else
                 begin
                   Date.parse(value)
                 rescue
                   raise(ArgumentError, "Cannot parse argument `#{value}` to Date.")
                 end
               end

        _append_date(date.year, date.month, date.day)
      end

      #
      # appends time value.
      #
      #   require 'duckdb'
      #   db = DuckDB::Database.open
      #   con = db.connect
      #   con.query('CREATE TABLE times (time_value TIME)')
      #   appender = con.appender('times')
      #   appender.begin_row
      #   appender.append_time(Time.now)
      #   # or
      #   # appender.append_time('01:01:01')
      #   appender.end_row
      #   appender.flush
      #
      def append_time(value)
        time = case value
               when Time
                 value
               else
                 begin
                   Time.parse(value)
                 rescue
                   raise(ArgumentError, "Cannot parse argument `#{value}` to Time.")
                 end
               end

        _append_time(time.hour, time.min, time.sec, time.usec)
      end

      #
      # appends timestamp value.
      #
      #   require 'duckdb'
      #   db = DuckDB::Database.open
      #   con = db.connect
      #   con.query('CREATE TABLE timestamps (timestamp_value TIMESTAMP)')
      #   appender = con.appender('timestamps')
      #   appender.begin_row
      #   appender.append_time(Time.now)
      #   # or
      #   # appender.append_time(Date.today)
      #   # appender.append_time('2021-08-01 01:01:01')
      #   appender.end_row
      #   appender.flush
      #
      def append_timestamp(value)
        time = case value
               when Time
                 value
               when Date
                 value.to_time
               else
                 begin
                   Time.parse(value)
                 rescue
                   raise(ArgumentError, "Cannot parse argument `#{value}` to Time or Date.")
                 end
               end

        _append_timestamp(time.year, time.month, time.day, time.hour, time.min, time.sec, time.nsec / 1000)
      end

      #
      # appends interval.
      # The argument must be ISO8601 duration format.
      # WARNING: This method is expremental.
      #
      #   require 'duckdb'
      #   db = DuckDB::Database.open
      #   con = db.connect
      #   con.query('CREATE TABLE intervals (interval_value INTERVAL)')
      #   appender = con.appender('intervals')
      #   appender
      #     .begin_row
      #     .append_interval('P1Y2D') # => append 1 year 2 days interval.
      #     .end_row
      #     .flush
      #
      def append_interval(value)
        raise ArgumentError, "Argument `#{value}` must be a string." unless value.is_a?(String)

        hash = iso8601_interval_to_hash(value)

        months, days, micros = hash_to__append_interval_args(hash)

        _append_interval(months, days, micros)
      end

      #
      # appends value.
      #
      #   require 'duckdb'
      #   db = DuckDB::Database.open
      #   con = db.connect
      #   con.query('CREATE TABLE users (id INTEGER, name VARCHAR)')
      #   appender = con.appender('users')
      #   appender.begin_row
      #   appender.append(1)
      #   appender.append('Alice')
      #   appender.end_row
      #
      def append(value)
        case value
        when NilClass
          append_null
        when Float
          append_double(value)
        when Integer
          case value
          when RANGE_INT16
            append_int16(value)
          when RANGE_INT32
            append_int32(value)
          when RANGE_INT64
            append_int64(value)
          else
            append_hugeint(value)
          end
        when String
          blob?(value) ? append_blob(value) : append_varchar(value)
        when TrueClass, FalseClass
          append_bool(value)
        when Time
          if respond_to?(:append_timestamp)
            append_timestamp(value)
          else
            append_varchar(value.strftime('%Y-%m-%d %H:%M:%S.%N'))
          end
        when Date
          if respond_to?(:append_date)
            append_date(value)
          else
            append_varchar(value.strftime('%Y-%m-%d'))
          end
        else
          raise(DuckDB::Error, "not supported type #{value} (#{value.class})")
        end
      end

      #
      # append a row.
      #
      #   appender.append_row(1, 'Alice')
      #
      # is same as:
      #
      #   appender.begin_row
      #   appender.append(1)
      #   appender.append('Alice')
      #   appender.end_row
      #
      def append_row(*args)
        begin_row
        args.each do |arg|
          append(arg)
        end
        end_row
      end

      private

      def blob?(value)
        value.instance_of?(DuckDB::Blob) || value.encoding == Encoding::BINARY
      end

      def iso8601_interval_to_hash(value)
        digit = ''
        time = false
        hash = {}
        hash.default = 0

        value.each_char do |c|
          if '-0123456789.'.include?(c)
            digit += c
          elsif c == 'T'
            time = true
            digit = ''
          elsif c == 'M'
            m_interval_to_hash(hash, digit, time)
            digit = ''
          elsif c == 'S'
            s_interval_to_hash(hash, digit)
            digit = ''
          elsif 'YDH'.include?(c)
            hash[c] = digit.to_i
            digit = ''
          elsif c != 'P'
            raise ArgumentError, "The argument `#{value}` can't be parse."
          end
        end
        hash
      end

      def m_interval_to_hash(hash, digit, time)
        key = time ? 'TM' : 'M'
        hash[key] = digit.to_i
      end

      def s_interval_to_hash(hash, digit)
        sec, msec = digit.split('.')
        hash['S'] = sec.to_i
        hash['MS'] = "#{msec}000000"[0, 6].to_i
        hash['MS'] *= -1 if hash['S'].negative?
      end

      def hash_to__append_interval_args(hash)
        months = hash['Y'] * 12 + hash['M']
        days = hash['D']
        micros = (hash['H'] * 3600 + hash['TM'] * 60 + hash['S']) * 1_000_000 + hash['MS']
        [months, days, micros]
      end
    end
  end
end
