require 'gir_ffi/builders/argument_builder'
require 'gir_ffi/variable_name_generator'
require 'gir_ffi/field_argument_info'

module GirFFI
  module Builders
    # Creates field getter and setter code for a given IFieldInfo.
    class FieldBuilder
      # Convertor for fields for field getters. Used when building getter
      # methods.
      class GetterArgumentBuilder < BaseArgumentBuilder
        def initialize(var_gen, field_argument_info, field_info, options = {})
          super(var_gen, field_argument_info)
          @field_info = field_info
          @length_arg = options.fetch(:length_argument) { NullArgumentBuilder.new }
        end

        def pre_conversion
          [
            "#{field_ptr} = @struct.to_ptr + #{field_offset}",
            "#{typed_ptr} = GirFFI::InOutPointer.new(#{field_type_tag}, #{field_ptr})",
            "#{bare_value} = #{typed_ptr}.to_value"
          ]
        end

        def capture_variable_name
          nil
        end

        def post_converted_name
          @post_converted_name ||= if has_post_conversion?
                                     new_variable
                                   else
                                     bare_value
                                   end
        end

        def return_value_name
          post_converted_name
        end

        def post_conversion
          if has_post_conversion?
            ["#{post_converted_name} = #{post_convertor.conversion}"]
          else
            []
          end
        end

        private

        def field_offset
          @field_info.offset
        end

        def field_ptr
          @field_ptr ||= @var_gen.new_var
        end

        def typed_ptr
          @typed_ptr ||= @var_gen.new_var
        end

        def bare_value
          @bare_value ||= @var_gen.new_var
        end

        def field_type_tag
          @field_type_tag ||= @field_info.field_type.tag_or_class.inspect
        end

        def field_type
          @field_type ||= @field_info.field_type
        end

        def has_post_conversion?
          type_info.needs_c_to_ruby_conversion_for_functions?
        end

        def post_convertor
          @post_convertor ||= CToRubyConvertor.new(type_info,
                                                   bare_value,
                                                   length_arg.post_converted_name)
        end
      end

      # Class to represent argument info for the argument of a getter method.
      # Implements the necessary parts of IArgumentInfo's interface.
      class GetterArgumentInfo
        attr_reader :name, :argument_type

        def initialize(name, type)
          @name = name
          @argument_type = type
        end

        def closure
          -1
        end

        def direction
          :out
        end

        def ownership_transfer
          :everything
        end

        def caller_allocates?
          false
        end

        def skip?
          false
        end
      end

      # Builder for field getters
      class GetterBuilder
        def initialize(info)
          @info = info
        end

        def method_definition
          template.method_definition
        end

        def singleton_method?
          false
        end

        def method_name
          @info.name
        end

        def method_arguments
          []
        end

        def preparation
          []
        end

        def invocation
          nil
        end

        def result
          [getter_argument_builder.return_value_name]
        end

        private

        def var_gen
          @var_gen ||= VariableNameGenerator.new
        end

        def template
          @template ||= MethodTemplate.new(self, argument_builders)
        end

        def argument_builders
          @argument_builders ||=
            ArgumentBuilderCollection.new(
              NullReturnValueBuilder.new,
              [getter_argument_builder, length_argument_builder])
        end

        def getter_argument_builder
          @getter_argument_builder ||=
            GetterArgumentBuilder.new(var_gen, field_argument_info, @info,
                                      length_argument: length_argument_builder)
        end

        def length_argument_builder
          @length_argument_builder ||=
            if array_length_field
              GetterArgumentBuilder.new(var_gen, length_argument_info, array_length_field)
            else
              NullArgumentBuilder.new
            end
        end

        def array_length_field
          @info.related_array_length_field
        end

        def length_argument_info
          @length_argument_info ||=
            GetterArgumentInfo.new 'length', array_length_field.field_type
        end

        def field_offset
          @info.offset
        end

        def field_type_tag
          @field_type_tag ||= @info.field_type.tag_or_class.inspect
        end

        def field_type
          @field_type ||= @info.field_type
        end

        def field_argument_info
          @field_argument_info ||= GetterArgumentInfo.new 'value', field_type
        end
      end

      attr_reader :info

      def initialize(field_info)
        @info = field_info
      end

      def build
        setup_getter
        setup_setter
      end

      def setup_getter
        container_class.class_eval getter_def unless container_defines_getter_method?
      end

      def container_defines_getter_method?
        container_info.find_instance_method info.name
      end

      def setup_setter
        container_class.class_eval setter_def if info.writable?
      end

      def getter_def
        getter_builder = GetterBuilder.new(info)
        getter_builder.method_definition
      end

      # TODO: Use MethodTemplate
      def setter_def
        builder = setter_builder

        field_ptr = builder.new_variable
        typed_ptr = builder.new_variable

        <<-CODE.reset_indentation
        def #{info.name}= #{builder.method_argument_name}
          #{field_ptr} = @struct.to_ptr + #{info.offset}
          #{typed_ptr} = GirFFI::InOutPointer.new(#{field_type_tag}, #{field_ptr})
          #{builder.pre_conversion.join("\n          ")}
          #{typed_ptr}.set_value #{builder.call_argument_name}
        end
        CODE
      end

      private

      def field_type_tag
        @field_type_tag ||= info.field_type.tag_or_class.inspect
      end

      def container_class
        @container_class ||= container_module.const_get(container_info.safe_name)
      end

      def container_module
        @container_module ||= Object.const_get(container_info.safe_namespace)
      end

      def container_info
        @container_info ||= info.container
      end

      def field_type
        @field_type ||= @info.field_type
      end

      def field_argument_info
        @field_argument_info ||= FieldArgumentInfo.new 'value', field_type
      end

      def setter_builder
        @setter_builder ||= ArgumentBuilder.new(VariableNameGenerator.new,
                                                field_argument_info)
      end
    end
  end
end
