# Protocol Buffers - Google's data interchange format
# Copyright 2022 Google Inc.  All rights reserved.
#
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd

##
# Implementation details below are subject to breaking changes without
# warning and are intended for use only within the gem.
module Google
  module Protobuf
    module Internal
      module Convert

        # Arena should be the
        # @param value [Object] Value to convert
        # @param arena [Arena] Arena that owns the Message where the MessageValue
        #   will be set
        # @return [Google::Protobuf::FFI::MessageValue]
        def convert_ruby_to_upb(value, arena, c_type, msg_or_enum_def)
          raise ArgumentError.new "Expected Descriptor or EnumDescriptor, instead got #{msg_or_enum_def.class}" unless [NilClass, Descriptor, EnumDescriptor].include? msg_or_enum_def.class
          return_value = Google::Protobuf::FFI::MessageValue.new
          case c_type
          when :float
            raise TypeError.new "Expected number type for float field '#{name}' (given #{value.class})." unless value.respond_to? :to_f
            return_value[:float_val] = value.to_f
          when :double
            raise TypeError.new "Expected number type for double field '#{name}' (given #{value.class})." unless value.respond_to? :to_f
            return_value[:double_val] = value.to_f
          when :bool
            raise TypeError.new "Invalid argument for boolean field '#{name}' (given #{value.class})." unless [TrueClass, FalseClass].include? value.class
            return_value[:bool_val] = value
          when :string
            raise TypeError.new "Invalid argument for string field '#{name}' (given #{value.class})." unless [Symbol, String].include? value.class
            begin
              string_value = value.to_s.encode("UTF-8")
            rescue Encoding::UndefinedConversionError
              # TODO - why not include the field name here?
              raise Encoding::UndefinedConversionError.new "String is invalid UTF-8"
            end
            return_value[:str_val][:size] = string_value.bytesize
            return_value[:str_val][:data] = Google::Protobuf::FFI.arena_malloc(arena, string_value.bytesize)
            # TODO - how important is it to still use arena malloc, versus the following?
            # buffer = ::FFI::MemoryPointer.new(:char, string_value.bytesize)
            # buffer.put_bytes(0, string_value)
            # return_value[:str_val][:data] = buffer
            raise NoMemoryError.new "Cannot allocate #{string_value.bytesize} bytes for string on Arena" if return_value[:str_val][:data].nil? || return_value[:str_val][:data].null?
            return_value[:str_val][:data].write_string(string_value)
          when :bytes
            raise TypeError.new "Invalid argument for bytes field '#{name}' (given #{value.class})." unless value.is_a? String
            string_value = value.encode("ASCII-8BIT")
            return_value[:str_val][:size] = string_value.bytesize
            return_value[:str_val][:data] = Google::Protobuf::FFI.arena_malloc(arena, string_value.bytesize)
            raise NoMemoryError.new "Cannot allocate #{string_value.bytesize} bytes for bytes on Arena" if return_value[:str_val][:data].nil? || return_value[:str_val][:data].null?
            return_value[:str_val][:data].write_string_length(string_value, string_value.bytesize)
          when :message
            raise TypeError.new "nil message not allowed here." if value.nil?
            if value.is_a? Hash
              raise RuntimeError.new "Attempted to initialize message from Hash for field #{name} but have no definition" if msg_or_enum_def.nil?
              new_message = msg_or_enum_def.msgclass.
                send(:private_constructor, arena, initial_value: value)
              return_value[:msg_val] = new_message.instance_variable_get(:@msg)
              return return_value
            end

            descriptor = value.class.respond_to?(:descriptor) ? value.class.descriptor : nil
            if descriptor != msg_or_enum_def
              wkt = Google::Protobuf::FFI.get_well_known_type(msg_or_enum_def)
              case wkt
              when :Timestamp
                raise TypeError.new "Invalid type #{value.class} to assign to submessage field '#{name}'." unless value.kind_of? Time
                new_message = Google::Protobuf::FFI.new_message_from_def msg_or_enum_def, arena
                sec = Google::Protobuf::FFI::MessageValue.new
                sec[:int64_val] = value.tv_sec
                sec_field_def = Google::Protobuf::FFI.get_field_by_number msg_or_enum_def, 1
                raise "Should be impossible" unless Google::Protobuf::FFI.set_message_field new_message, sec_field_def, sec, arena
                nsec_field_def = Google::Protobuf::FFI.get_field_by_number msg_or_enum_def, 2
                nsec = Google::Protobuf::FFI::MessageValue.new
                nsec[:int32_val] = value.tv_nsec
                raise "Should be impossible" unless Google::Protobuf::FFI.set_message_field new_message, nsec_field_def, nsec, arena
                return_value[:msg_val] = new_message
              when :Duration
                raise TypeError.new "Invalid type #{value.class} to assign to submessage field '#{name}'." unless value.kind_of? Numeric
                new_message = Google::Protobuf::FFI.new_message_from_def msg_or_enum_def, arena
                sec = Google::Protobuf::FFI::MessageValue.new
                sec[:int64_val] = value
                sec_field_def = Google::Protobuf::FFI.get_field_by_number msg_or_enum_def, 1
                raise "Should be impossible" unless Google::Protobuf::FFI.set_message_field new_message, sec_field_def, sec, arena
                nsec_field_def = Google::Protobuf::FFI.get_field_by_number msg_or_enum_def, 2
                nsec = Google::Protobuf::FFI::MessageValue.new
                nsec[:int32_val] = ((value.to_f - value.to_i) * 1000000000).round
                raise "Should be impossible" unless Google::Protobuf::FFI.set_message_field new_message, nsec_field_def, nsec, arena
                return_value[:msg_val] = new_message
              else
                raise TypeError.new "Invalid type #{value.class} to assign to submessage field '#{name}'."
              end
            else
              arena.fuse(value.instance_variable_get(:@arena))
              return_value[:msg_val] = value.instance_variable_get :@msg
            end
          when :enum
            return_value[:int32_val] = case value
              when Numeric
                value.to_i
              when String, Symbol
                enum_number = EnumDescriptor.send(:lookup_name, msg_or_enum_def, value.to_s)
                #TODO add the bad value to the error message after tests pass
                raise RangeError.new "Unknown symbol value for enum field '#{name}'." if enum_number.nil?
                enum_number
              else
                raise TypeError.new "Expected number or symbol type for enum field '#{name}'."
              end
          #TODO After all tests pass, improve error message across integer type by including actual offending value
          when :int32
            raise TypeError.new "Expected number type for integral field '#{name}' (given #{value.class})." unless value.is_a? Numeric
            raise RangeError.new "Non-integral floating point value assigned to integer field '#{name}' (given #{value.class})." if value.floor != value
            raise RangeError.new "Value assigned to int32 field '#{name}' (given #{value.class}) with more than 32-bits." unless value.to_i.bit_length < 32
            return_value[:int32_val] = value.to_i
          when :uint32
            raise TypeError.new "Expected number type for integral field '#{name}' (given #{value.class})." unless value.is_a? Numeric
            raise RangeError.new "Non-integral floating point value assigned to integer field '#{name}' (given #{value.class})." if value.floor != value
            raise RangeError.new "Assigning negative value to unsigned integer field '#{name}' (given #{value.class})." if value < 0
            raise RangeError.new "Value assigned to uint32 field '#{name}' (given #{value.class}) with more than 32-bits." unless value.to_i.bit_length < 33
            return_value[:uint32_val] = value.to_i
          when :int64
            raise TypeError.new "Expected number type for integral field '#{name}' (given #{value.class})." unless value.is_a? Numeric
            raise RangeError.new "Non-integral floating point value assigned to integer field '#{name}' (given #{value.class})." if value.floor != value
            raise RangeError.new "Value assigned to int64 field '#{name}' (given #{value.class}) with more than 64-bits." unless value.to_i.bit_length < 64
            return_value[:int64_val] = value.to_i
          when :uint64
            raise TypeError.new "Expected number type for integral field '#{name}' (given #{value.class})." unless value.is_a? Numeric
            raise RangeError.new "Non-integral floating point value assigned to integer field '#{name}' (given #{value.class})." if value.floor != value
            raise RangeError.new "Assigning negative value to unsigned integer field '#{name}' (given #{value.class})." if value < 0
            raise RangeError.new "Value assigned to uint64 field '#{name}' (given #{value.class}) with more than 64-bits." unless value.to_i.bit_length < 65
            return_value[:uint64_val] = value.to_i
          else
            raise RuntimeError.new "Unsupported type #{c_type}"
          end
          return_value
        end

        ##
        # Safe to call without an arena if the caller has checked that c_type
        # is not :message.
        # @param message_value [Google::Protobuf::FFI::MessageValue] Value to be converted.
        # @param c_type [Google::Protobuf::FFI::CType] Enum representing the type of message_value
        # @param msg_or_enum_def [::FFI::Pointer] Pointer to the MsgDef or EnumDef definition
        # @param arena [Google::Protobuf::Internal::Arena] Arena to create Message instances, if needed
        def convert_upb_to_ruby(message_value, c_type, msg_or_enum_def = nil, arena = nil)
          throw TypeError.new "Expected MessageValue but got #{message_value.class}" unless message_value.is_a? Google::Protobuf::FFI::MessageValue

          case c_type
          when :bool
            message_value[:bool_val]
          when :int32
            message_value[:int32_val]
          when :uint32
            message_value[:uint32_val]
          when :double
            message_value[:double_val]
          when :int64
            message_value[:int64_val]
          when :uint64
            message_value[:uint64_val]
          when :string
            if message_value[:str_val][:size].zero?
              ""
            else
              message_value[:str_val][:data].read_string_length(message_value[:str_val][:size]).force_encoding("UTF-8").freeze
            end
          when :bytes
            if message_value[:str_val][:size].zero?
              ""
            else
              message_value[:str_val][:data].read_string_length(message_value[:str_val][:size]).force_encoding("ASCII-8BIT").freeze
            end
          when :float
            message_value[:float_val]
          when :enum
            EnumDescriptor.send(:lookup_value, msg_or_enum_def, message_value[:int32_val]) || message_value[:int32_val]
          when :message
            raise "Null Arena for message" if arena.nil?
            Descriptor.send(:get_message, message_value[:msg_val], msg_or_enum_def, arena)
          else
            raise RuntimeError.new "Unexpected type #{c_type}"
          end
        end

        def to_h_internal(msg, message_descriptor)
          return nil if msg.nil? or msg.null?
          hash = {}
          iter = ::FFI::MemoryPointer.new(:size_t, 1)
          iter.write(:size_t, Google::Protobuf::FFI::Upb_Message_Begin)
          message_value = Google::Protobuf::FFI::MessageValue.new
          field_def_ptr = ::FFI::MemoryPointer.new :pointer

          while Google::Protobuf::FFI::message_next(msg, message_descriptor, nil, field_def_ptr, message_value, iter) do
            field_descriptor = FieldDescriptor.from_native field_def_ptr.get_pointer(0)

            if field_descriptor.map?
              hash_entry = map_create_hash(message_value[:map_val], field_descriptor)
            elsif field_descriptor.repeated?
              hash_entry = repeated_field_create_array(message_value[:array_val], field_descriptor, field_descriptor.type)
            else
              hash_entry = scalar_create_hash(message_value, field_descriptor.type, field_descriptor: field_descriptor)
            end

            hash[field_descriptor.name.to_sym] = hash_entry
          end

          hash
        end

        def map_create_hash(map_ptr, field_descriptor)
          return {} if map_ptr.nil? or map_ptr.null?
          return_value = {}

          message_descriptor = field_descriptor.send(:subtype)
          key_field_def = Google::Protobuf::FFI.get_field_by_number(message_descriptor, 1)
          key_field_type = Google::Protobuf::FFI.get_type(key_field_def)

          value_field_def = Google::Protobuf::FFI.get_field_by_number(message_descriptor, 2)
          value_field_type = Google::Protobuf::FFI.get_type(value_field_def)

          iter = ::FFI::MemoryPointer.new(:size_t, 1)
          iter.write(:size_t, Google::Protobuf::FFI::Upb_Map_Begin)
          while Google::Protobuf::FFI.map_next(map_ptr, iter) do
            iter_size_t = iter.read(:size_t)
            key_message_value = Google::Protobuf::FFI.map_key(map_ptr, iter_size_t)
            value_message_value = Google::Protobuf::FFI.map_value(map_ptr, iter_size_t)
            hash_key = convert_upb_to_ruby(key_message_value, key_field_type)
            hash_value = scalar_create_hash(value_message_value, value_field_type, msg_or_enum_descriptor: value_field_def.subtype)
            return_value[hash_key] = hash_value
          end
          return_value
        end

        def repeated_field_create_array(array, field_descriptor, type)
          return_value = []
          n = (array.nil? || array.null?) ? 0 : Google::Protobuf::FFI.array_size(array)
          0.upto(n - 1) do |i|
            message_value = Google::Protobuf::FFI.get_msgval_at(array, i)
            return_value << scalar_create_hash(message_value, type, field_descriptor: field_descriptor)
          end
          return_value
        end

        # @param field_descriptor [FieldDescriptor] Descriptor of the field to convert to a hash.
        def scalar_create_hash(message_value, type, field_descriptor: nil, msg_or_enum_descriptor: nil)
          if [:message, :enum].include? type
            if field_descriptor.nil?
              if msg_or_enum_descriptor.nil?
                raise "scalar_create_hash requires either a FieldDescriptor, MessageDescriptor, or EnumDescriptor as an argument, but received only nil"
              end
            else
              msg_or_enum_descriptor = field_descriptor.subtype
            end
            if type == :message
              to_h_internal(message_value[:msg_val], msg_or_enum_descriptor)
            elsif type == :enum
              convert_upb_to_ruby message_value, type, msg_or_enum_descriptor
            end
          else
            convert_upb_to_ruby message_value, type
          end
        end

        def message_value_deep_copy(message_value, type, descriptor, arena)
          raise unless message_value.is_a? Google::Protobuf::FFI::MessageValue
          new_message_value = Google::Protobuf::FFI::MessageValue.new
          case type
          when :string, :bytes
            # TODO - how important is it to still use arena malloc, versus using FFI MemoryPointers?
            new_message_value[:str_val][:size] = message_value[:str_val][:size]
            new_message_value[:str_val][:data] = Google::Protobuf::FFI.arena_malloc(arena, message_value[:str_val][:size])
            raise NoMemoryError.new "Allocation failed" if  new_message_value[:str_val][:data].nil? or new_message_value[:str_val][:data].null?
            Google::Protobuf::FFI.memcpy(new_message_value[:str_val][:data], message_value[:str_val][:data], message_value[:str_val][:size])
          when :message
            new_message_value[:msg_val] = descriptor.msgclass.send(:deep_copy, message_value[:msg_val], arena).instance_variable_get(:@msg)
          else
            Google::Protobuf::FFI.memcpy(new_message_value.to_ptr, message_value.to_ptr, Google::Protobuf::FFI::MessageValue.size)
          end
          new_message_value
        end
      end
    end
  end
end
