# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

require "date"

module Arrow
  class ArrayBuilder
    class << self
      def build(values)
        if self != ArrayBuilder
          builder = new
          return builder.build(values)
        end

        builder_info = nil
        values.each do |value|
          builder_info = detect_builder_info(value, builder_info)
          break if builder_info and builder_info[:detected]
        end
        if builder_info
          builder = builder_info[:builder]
          if builder.nil? and builder_info[:builder_type]
            builder = create_builder(builder_info)
          end
        end
        if builder
          builder.build(values)
        else
          Arrow::StringArray.new(values)
        end
      end

      def buildable?(args)
        args.size == method(:build).arity
      end

      private
      def detect_builder_info(value, builder_info)
        case value
        when nil
          builder_info
        when true, false
          {
            builder: BooleanArrayBuilder.new,
            detected: true,
          }
        when String
          {
            builder: StringArrayBuilder.new,
            detected: true,
          }
        when Symbol
          {
            builder: StringDictionaryArrayBuilder.new,
            detected: true,
          }
        when Float
          {
            builder: DoubleArrayBuilder.new,
            detected: true,
          }
        when Integer
          builder_info ||= {}
          min = builder_info[:min] || value
          max = builder_info[:max] || value
          min = value if value < min
          max = value if value > max

          if builder_info[:builder_type] == :int || value < 0
            {
              builder_type: :int,
              min: min,
              max: max,
            }
          else
            {
              builder_type: :uint,
              min: min,
              max: max,
            }
          end
        when Time
          data_type = value.data_type
          case data_type.unit
          when TimeUnit::SECOND
            builder_info || {
              builder: Time32ArrayBuilder.new(data_type)
            }
          when TimeUnit::MILLI
            if builder_info and builder_info[:builder].is_a?(Time64ArrayBuilder)
              builder_info
            else
              {
                builder: Time32ArrayBuilder.new(data_type),
              }
            end
          when TimeUnit::MICRO
            {
              builder: Time64ArrayBuilder.new(data_type),
            }
          when TimeUnit::NANO
            {
              builder: Time64ArrayBuilder.new(data_type),
              detected: true
            }
          end
        when ::Time
          data_type = TimestampDataType.new(:nano)
          {
            builder: TimestampArrayBuilder.new(data_type),
            detected: true,
          }
        when DateTime
          {
            builder: Date64ArrayBuilder.new,
            detected: true,
          }
        when Date
          {
            builder: Date32ArrayBuilder.new,
            detected: true,
          }
        when BigDecimal
          builder_info ||= {}
          if builder_info[:builder] or value.nan? or value.infinite?
            {
              builder: StringArrayBuilder.new,
              detected: true,
            }
          else
            precision = [builder_info[:precision] || 0, value.precision].max
            scale = [builder_info[:scale] || 0, value.scale].max
            if precision <= Decimal128DataType::MAX_PRECISION
              {
                builder_type: :decimal128,
                precision: precision,
                scale: scale,
              }
            else
              {
                builder_type: :decimal256,
                precision: precision,
                scale: scale,
              }
            end
          end
        when ::Array
          sub_builder_info = builder_info && builder_info[:value_builder_info]
          value.each do |sub_value|
            sub_builder_info = detect_builder_info(sub_value, sub_builder_info)
            break if sub_builder_info and sub_builder_info[:detected]
          end
          if sub_builder_info
            sub_builder = sub_builder_info[:builder] || create_builder(sub_builder_info)
            return sub_builder_info unless sub_builder
            sub_value_data_type = sub_builder.value_data_type
            field = Field.new("item", sub_value_data_type)
            {
              builder: ListArrayBuilder.new(ListDataType.new(field)),
              value_builder_info: sub_builder_info,
              detected: sub_builder_info[:detected],
            }
          else
            builder_info
          end
        else
          {
            builder: StringArrayBuilder.new,
            detected: true,
          }
        end
      end

      def create_builder(builder_info)
        builder_type = builder_info[:builder_type]
        case builder_type
        when :decimal128
          data_type = Decimal128DataType.new(builder_info[:precision],
                                             builder_info[:scale])
          Decimal128ArrayBuilder.new(data_type)
        when :decimal256
          data_type = Decimal256DataType.new(builder_info[:precision],
                                             builder_info[:scale])
          Decimal256ArrayBuilder.new(data_type)
        when :int
          min = builder_info[:min]
          max = builder_info[:max]

          if GLib::MININT8 <= min && max <= GLib::MAXINT8
            Int8ArrayBuilder.new
          elsif GLib::MININT16 <= min && max <= GLib::MAXINT16
            Int16ArrayBuilder.new
          elsif GLib::MININT32 <= min && max <= GLib::MAXINT32
            Int32ArrayBuilder.new
          elsif GLib::MININT64 <= min && max <= GLib::MAXINT64
            Int64ArrayBuilder.new
          else
            StringArrayBuilder.new
          end
        when :uint
          max = builder_info[:max]

          if max <= GLib::MAXUINT8
            UInt8ArrayBuilder.new
          elsif max <= GLib::MAXUINT16
            UInt16ArrayBuilder.new
          elsif max <= GLib::MAXUINT32
            UInt32ArrayBuilder.new
          elsif max <= GLib::MAXUINT64
            UInt64ArrayBuilder.new
          else
            StringArrayBuilder.new
          end
        else
          nil
        end
      end
    end

    def build(values)
      append(*values)
      finish
    end

    # @since 0.12.0
    def append(*values)
      value_convertable = respond_to?(:convert_to_arrow_value, true)
      start_index = 0
      current_index = 0
      status = :value

      values.each do |value|
        if value.nil?
          if status == :value
            if start_index != current_index
              target_values = values[start_index...current_index]
              if value_convertable
                target_values = target_values.collect do |v|
                  convert_to_arrow_value(v)
                end
              end
              append_values(target_values, nil)
              start_index = current_index
            end
            status = :null
          end
        else
          if status == :null
            append_nulls(current_index - start_index)
            start_index = current_index
            status = :value
          end
        end
        current_index += 1
      end
      if start_index != current_index
        if status == :value
          if start_index == 0 and current_index == values.size
            target_values = values
          else
            target_values = values[start_index...current_index]
          end
          if value_convertable
            target_values = target_values.collect do |v|
              convert_to_arrow_value(v)
            end
          end
          append_values(target_values, nil)
        else
          append_nulls(current_index - start_index)
        end
      end
    end
  end
end
