require "ffi"

class NDAV
  module FFI
    TYPE_SIZE_TO_FORMAT = {
      1 => "c",
      2 => "s",
      4 => "l",
      8 => "q"
    }

    module MemoryPointer
      module FromNDAV
        # NOTE: Copies data
        def from_ndav(array)
          from_string(array.to_s)
        end
      end

      module ToNDAV
        def to_ndav(format: TYPE_SIZE_TO_FORMAT[type_size], lifetime: self, **)
          shape = [size / type_size]
          byte_size = shape.reduce(ITEM_SIZES[format], :*)
          ptr = ::Fiddle::Pointer.new(address, byte_size)
          ::NDAV.new(ptr, shape:, format:, lifetime:, **)
        end
      end

      module MemoryViewable
        def ndav_descriptor(**)
          shape = [size / type_size]
          format = TYPE_SIZE_TO_FORMAT[type_size]
          item_size = ITEM_SIZES[format]
          byte_size = shape.reduce(item_size, :*)
          ptr = ::Fiddle::Pointer.new(address, byte_size)

          {
            data: Fiddle::MemoryView.new(ptr).to_s,
            shape:,
            strides: ::NDAV.default_strides(shape:, item_size:),
            format:,
            byte_size:,
            readonly?: true
          }
        end
      end

      module Converter
        def FFIMemoryPointer(array, *, **)
          if ::FFI::MemoryPointer === array
            array
          elsif array.respond_to? :to_ffi_memory_pointer
            array.to_ffi_memory_pointer(*, **)
          elsif array.respond_to? :to_ndav
            array.to_ndav(*, **).to_ffi_memory_pointer
          else
            ::NDAV.new(array, *, **).to_ffi_memory_pointer
          end
        end
      end
    end

    module Pointer
      module FromNDAV
        def from_ndav(array)
          new(array.item_size, array.to_ptr.to_i)
        end
      end

      module ToNDAV
        def to_ndav(shape:, format: TYPE_SIZE_TO_FORMAT[type_size], lifetime: self, **)
          byte_size = shape.reduce(ITEM_SIZES[format], :*)
          ptr = ::Fiddle::Pointer.new(address, byte_size)
          ::NDAV.new(ptr, shape:, format:, lifetime:, **)
        end
      end

      module Converter
        def FFIPointer(array, *, **)
          # use instance_of? instead of kind_of? beacuase MemoryPointer is subclass of Pointer
          if array.instance_of? ::FFI::Pointer
            array
          elsif array.respond_to? :to_ffi_pointer
            array.to_ffi_pointer(*, **)
          elsif array.respond_to? :to_ndav
            array.to_ndav(*, **).to_ffi_pointer
          else
            ::NDAV.new(array, *, **).to_ffi_pointer
          end
        end
      end
    end

    ::NDAV.register ::FFI::MemoryPointer, MemoryPointer, name: :ffi_memory_pointer
    ::NDAV.register ::FFI::Pointer, Pointer, name: :ffi_pointer
  end
end