# frozen_string_literal: true

require "gir_ffi/builders/base_argument_builder"
require "gir_ffi/builders/c_to_ruby_convertor"
require "gir_ffi/builders/closure_convertor"
require "gir_ffi/builders/null_convertor"
require "gir_ffi/builders/pointer_value_convertor"

module GirFFI
  module Builders
    # Convertor for arguments for ruby callbacks. Used when building the
    # argument mapper for callbacks.
    class CallbackArgumentBuilder < BaseArgumentBuilder
      def method_argument_name
        @method_argument_name ||= name || new_variable
      end

      def block_argument?
        false
      end

      # All arguments to the argument mapper must always be provided. They may
      # be nil, though.
      def allow_none?
        false
      end

      def pre_converted_name
        @pre_converted_name ||= new_variable
      end

      def out_parameter_name
        @out_parameter_name ||=
          if direction == :inout
            new_variable
          else
            pre_converted_name
          end
      end

      def call_argument_name
        pre_converted_name if [:in, :inout].include?(direction) && !array_arg
      end

      def capture_variable_name
        result_name if [:out, :inout].include?(direction) && !array_arg
      end

      def pre_conversion
        case direction
        when :in
          [ingoing_pre_conversion]
        when :out
          [out_parameter_preparation]
        when :inout
          [out_parameter_preparation, ingoing_pre_conversion]
        when :error
          [out_parameter_preparation, "begin"]
        end
      end

      def post_conversion
        case direction
        when :out, :inout
          [value_to_pointer_conversion]
        when :error
          [
            "rescue => #{result_name}",
            value_to_pointer_conversion,
            "end"
          ]
        else
          []
        end
      end

      private

      def result_name
        @result_name ||= new_variable
      end

      def pre_convertor_argument
        if direction == :inout
          pointer_to_value_conversion
        else
          method_argument_name
        end
      end

      def pointer_value_convertor
        @pointer_value_convertor ||= if allocated_by_us?
                                       PointerValueConvertor.new(type_spec[1])
                                     else
                                       PointerValueConvertor.new(type_spec)
                                     end
      end

      def pointer_to_value_conversion
        pointer_value_convertor.pointer_to_value(out_parameter_name)
      end

      def value_to_pointer_conversion
        pointer_value_convertor.value_to_pointer(out_parameter_name,
                                                 post_convertor.conversion)
      end

      def pre_convertor
        @pre_convertor ||= if user_data?
                             ClosureConvertor.new(pre_convertor_argument)
                           elsif needs_c_to_ruby_conversion?
                             CToRubyConvertor.new(type_info,
                                                  pre_convertor_argument,
                                                  length_argument_name)
                           else
                             NullConvertor.new(pre_convertor_argument)
                           end
      end

      def needs_c_to_ruby_conversion?
        type_info.needs_c_to_ruby_conversion_for_callbacks?
      end

      def ingoing_pre_conversion
        "#{pre_converted_name} = #{pre_convertor.conversion}"
      end

      def post_convertor
        @post_convertor ||= if type_info.needs_ruby_to_c_conversion_for_callbacks?
                              RubyToCConvertor.new(type_info, post_convertor_argument)
                            else
                              NullConvertor.new(post_convertor_argument)
                            end
      end

      def post_convertor_argument
        if array_arg
          "#{array_arg.capture_variable_name}.length"
        else
          result_name
        end
      end

      def out_parameter_preparation
        value = if allocated_by_us?
                  ffi_type = TypeMap.type_specification_to_ffi_type type_spec.last
                  "FFI::MemoryPointer.new(#{ffi_type.inspect})" \
                    ".tap { |ptr| #{method_argument_name}.put_pointer 0, ptr }"
                else
                  method_argument_name
                end
        "#{out_parameter_name} = #{value}"
      end

      def type_spec
        type_info.tag_or_class
      end

      # Check if an out argument needs to be allocated by us, the callee. Since
      # caller_allocates is false by default, we must also check that the type
      # is a pointer. For example, an out parameter of type gint8* will always
      # be allocate by the caller.
      def allocated_by_us?
        direction == :out &&
          !@arginfo.caller_allocates? &&
          type_info.pointer? &&
          ![:object, :zero_terminated].include?(specialized_type_tag)
      end

      def length_argument_name
        length_arg&.pre_converted_name
      end
    end
  end
end
