# -*- encoding: utf-8 -*-
#
# This file is part of ruby-ffi.
# For licensing, see LICENSE.SPECS
#

require File.expand_path(File.join(File.dirname(__FILE__), "spec_helper"))
require 'delegate'

module PointerTestLib
  extend FFI::Library
  ffi_lib TestLibrary::PATH
  begin
    attach_function :ptr_ret_int32_t, [ :pointer, :int ], :int
  rescue FFI::NotFoundError
    # NetBSD uses #define instead of typedef for these
    attach_function :ptr_ret_int32_t, :ptr_ret___int32_t, [ :pointer, :int ], :int
  end
  attach_function :ptr_ret_int64_t, [ :pointer, :int ], :int64_t
  attach_function :ptr_from_address, [ FFI::Platform::ADDRESS_SIZE == 32 ? :uint : :ulong_long ], :pointer
  attach_function :ptr_set_pointer, [ :pointer, :int, :pointer ], :void
  attach_function :ptr_ret_pointer, [ :pointer, :int ], :pointer
end
describe "Pointer" do
  include FFI
  class ToPtrTest
    def initialize(ptr)
      @ptr = ptr
    end
    def to_ptr
      @ptr
    end
  end

  it "Any object implementing #to_ptr can be passed as a :pointer parameter" do
    memory = FFI::MemoryPointer.new :long_long
    magic = 0x12345678
    memory.put_int32(0, magic)
    tp = ToPtrTest.new(memory)
    expect(PointerTestLib.ptr_ret_int32_t(tp, 0)).to eq(magic)
  end
  class PointerDelegate < DelegateClass(FFI::Pointer)
    def initialize(ptr)
      @ptr = ptr
    end
    def to_ptr
      @ptr
    end
  end

  it "A DelegateClass(Pointer) can be passed as a :pointer parameter" do
    memory = FFI::MemoryPointer.new :long_long
    magic = 0x12345678
    memory.put_int32(0, magic)
    ptr = PointerDelegate.new(memory)
    expect(PointerTestLib.ptr_ret_int32_t(ptr, 0)).to eq(magic)
  end

  it "Integer cannot be used as a Pointer argument" do
    expect { PointerTestLib.ptr_ret_int32_t(0, 0) }.to raise_error(ArgumentError)
  end

  it "Bignum cannot be used as a Pointer argument" do
    expect { PointerTestLib.ptr_ret_int32_t(0xfee1deadbeefcafebabe, 0) }.to raise_error(ArgumentError)
  end

  it "String can be used as a Pointer argument" do
    s = "123\0abö"
    expect( PointerTestLib.ptr_ret_int64_t(s, 0) ).to eq(s.unpack("q")[0])
  end

  it "#to_ptr" do
    memory = FFI::MemoryPointer.new :pointer
    expect(memory.to_ptr).to eq(memory)

    expect(FFI::Pointer::NULL.to_ptr).to eq(FFI::Pointer::NULL)
  end

  it "equals itself" do
    memory = FFI::MemoryPointer.new :pointer
    expect(memory == memory).to be true
  end

  it "does not equal non pointers" do
    memory = FFI::MemoryPointer.new :pointer
    expect(memory == Hash.new).to be false
  end

  describe "pointer type methods" do

    it "#read_pointer" do
      memory = FFI::MemoryPointer.new :pointer
      PointerTestLib.ptr_set_pointer(memory, 0, PointerTestLib.ptr_from_address(0xdeadbeef))
      expect(memory.read_pointer.address).to eq(0xdeadbeef)
    end

    it "#write_pointer" do
      memory = FFI::MemoryPointer.new :pointer
      memory.write_pointer(PointerTestLib.ptr_from_address(0xdeadbeef))
      expect(PointerTestLib.ptr_ret_pointer(memory, 0).address).to eq(0xdeadbeef)
    end

    it "#write_pointer frozen object" do
      skip "not yet supported on TruffleRuby" if RUBY_ENGINE == "truffleruby"
      skip "not yet supported on JRuby" if RUBY_ENGINE == "jruby"
      memory = FFI::MemoryPointer.new(:pointer).freeze
      expect{ memory.write_pointer(PointerTestLib.ptr_from_address(0xdeadbeef)) }.to raise_error(RuntimeError, /memory write/)
    end

    it "#read_array_of_pointer" do
      values = [0x12345678, 0xfeedf00d, 0xdeadbeef]
      memory = FFI::MemoryPointer.new :pointer, values.size
      values.each_with_index do |address, j|
        PointerTestLib.ptr_set_pointer(memory, j * FFI.type_size(:pointer), PointerTestLib.ptr_from_address(address))
      end
      array = memory.read_array_of_pointer(values.size)
      values.each_with_index do |address, j|
        expect(array[j].address).to eq(address)
      end
    end

    it "#write_array_of_type for uint8" do
      values = [10, 227, 32]
      memory = FFI::MemoryPointer.new FFI::TYPE_UINT8, values.size
      memory.write_array_of_type(FFI::TYPE_UINT8, :put_uint8, values)
      array = memory.read_array_of_type(FFI::TYPE_UINT8, :read_uint8, values.size)
      values.each_with_index do |val, j|
        expect(array[j]).to eq(val)
      end
    end

    it "#write_array_of_type for uint32" do
      values = [10, 227, 32]
      memory = FFI::MemoryPointer.new FFI::TYPE_UINT32, values.size
      memory.write_array_of_type(FFI::TYPE_UINT32, :put_uint32, values)
      array = memory.read_array_of_type(FFI::TYPE_UINT32, :read_uint32, values.size)
      values.each_with_index do |val, j|
        expect(array[j]).to eq(val)
      end
    end
    it "#write_array_of_type should raise an error with non-array argument" do
      memory = FFI::MemoryPointer.new FFI::TYPE_INT8, 1
      expect { memory.write_array_of_int8(0) }.to raise_error(TypeError)
    end
  end

  describe 'NULL' do
    it 'should be obtained using Pointer::NULL constant' do
      null_ptr = FFI::Pointer::NULL
      expect(null_ptr).to be_null
    end
    it 'should be obtained passing address 0 to constructor' do
      expect(FFI::Pointer.new(0)).to be_null
    end
    it 'should raise an error when attempting read/write operations on it' do
      null_ptr = FFI::Pointer::NULL
      expect { null_ptr.read_int }.to raise_error(FFI::NullPointerError)
      expect { null_ptr.write_int(0xff1) }.to raise_error(FFI::NullPointerError)
    end
    it 'returns true when compared with nil' do
      expect((FFI::Pointer::NULL == nil)).to be true
    end
    it 'returns false when compared with a non-pointer object' do
      expect((FFI::Pointer::NULL == Array.new)).to be false
    end
    it 'should not raise an error when attempting read/write zero length array' do
      null_ptr = FFI::Pointer::NULL
      expect( null_ptr.read_array_of_uint(0) ).to eq([])
      null_ptr.write_array_of_uint([])
    end
  end

  it "Pointer.size returns sizeof pointer on platform" do
    expect(FFI::Pointer.size).to eq((FFI::Platform::ADDRESS_SIZE / 8))
  end

  describe "#slice" do
    before(:each) do
      @mptr = FFI::MemoryPointer.new(:char, 12)
      @mptr.put_uint(0, 0x12345678)
      @mptr.put_uint(4, 0xdeadbeef)
    end

    it "contents of sliced pointer matches original pointer at offset" do
      expect(@mptr.slice(4, 4).get_uint(0)).to eq(0xdeadbeef)
    end

    it "modifying sliced pointer is reflected in original pointer" do
      @mptr.slice(4, 4).put_uint(0, 0xfee1dead)
      expect(@mptr.get_uint(4)).to eq(0xfee1dead)
    end

    it "access beyond bounds should raise IndexError" do
      expect { @mptr.slice(4, 4).get_int(4) }.to raise_error(IndexError)
    end
  end

  describe "#type_size" do
    it "should be same as FFI.type_size(type)" do
      expect(FFI::MemoryPointer.new(:int, 1).type_size).to eq(FFI.type_size(:int))
    end
  end

  describe "#order" do
    it "should return the system order by default" do
      expect(FFI::Pointer.new(0x0).order).to eq(OrderHelper::ORDER)
    end

    it "should return self if there is no change" do
      pointer = FFI::Pointer.new(0x0)
      expect(pointer.order(OrderHelper::ORDER)).to be pointer
    end

    it "should return a new pointer if there is a change" do
      pointer = FFI::Pointer.new(0x0)
      expect(pointer.order(OrderHelper::OTHER_ORDER)).to_not be pointer
    end

    it "can be set to :little" do
      expect(FFI::Pointer.new(0x0).order(:little).order).to eq(:little)
    end

    it "can be set to :big" do
      expect(FFI::Pointer.new(0x0).order(:big).order).to eq(:big)
    end

    it "can be set to :network, which sets it to :big" do
      expect(FFI::Pointer.new(0x0).order(:network).order).to eq(:big)
    end

    it "cannot be set to other symbols" do
      expect { FFI::Pointer.new(0x0).order(:unknown) }.to raise_error(ArgumentError)
    end

    it "can be used to read in little order" do
      pointer = FFI::MemoryPointer.from_string("\x1\x2\x3\x4").order(:little)
      expect(pointer.read_int32).to eq(67305985)
    end

    it "can be used to read in big order" do
      pointer = FFI::MemoryPointer.from_string("\x1\x2\x3\x4").order(:big)
      expect(pointer.read_int32).to eq(16909060)
    end

    it "can be used to read in network order" do
      pointer = FFI::MemoryPointer.from_string("\x1\x2\x3\x4").order(:network)
      expect(pointer.read_int32).to eq(16909060)
    end
  end if RUBY_ENGINE != "truffleruby"

  describe "#size_limit?" do
    it "should not have size limit" do
      expect(FFI::Pointer.new(0).size_limit?).to be false
    end

    it "should have size limit" do
      expect(FFI::Pointer.new(0).slice(0, 10).size_limit?).to be true
    end
  end

  describe "#initialize" do
    it 'can use addresses with high bit set' do
      max_address = 2**FFI::Platform::ADDRESS_SIZE - 1
      pointer = FFI::Pointer.new(:uint8, max_address)
      expect(pointer.address).to eq(max_address)
    end
  end if (RUBY_ENGINE != "truffleruby" && RUBY_ENGINE != "jruby")

  describe "#inspect" do
    it "should include the address" do
      expect(FFI::Pointer.new(1234).inspect).to match(/address=0x0*4d2/)
    end

    it "should not include the size if the pointer is unsized" do
      expect(FFI::Pointer.new(1234).inspect).not_to match(/size=/)
    end

    it "should include the size if there is one" do
      expect(FFI::MemoryPointer.new(:char, 16).inspect).to match(/size=16/)
    end
  end
end

describe "AutoPointer" do
  loop_count = 30
  wiggle_room = 5 # GC rarely cleans up all objects. we can get most of them, and that's enough to determine if the basic functionality is working.
  magic = 0x12345678

  class AutoPointerTestHelper
    @@count = 0
    def self.release
      @@count += 1 if @@count > 0
    end
    private_class_method(:release)
    def self.reset
      @@count = 0
    end
    def self.gc_everything(count)
      loop = 5
      while @@count < count && loop > 0
        loop -= 1
        TestLibrary.force_gc
        sleep 0.05 unless @@count == count
      end
      @@count = 0
    end
    def self.finalizer
      self.method(:release).to_proc
    end
  end
  class AutoPointerSubclass < FFI::AutoPointer
    def self.release(ptr); end
    private_class_method(:release)
  end

  # see #427
  it "cleanup via default release method", gc_dependent: true do
    expect(AutoPointerSubclass).to receive(:release).at_least(loop_count-wiggle_room).times
    AutoPointerTestHelper.reset
    loop_count.times do
      # note that if we called
      # AutoPointerTestHelper.method(:release).to_proc inline, we'd
      # have a reference to the pointer and it would never get GC'd.
      AutoPointerSubclass.new(PointerTestLib.ptr_from_address(magic))
    end
    AutoPointerTestHelper.gc_everything loop_count
  end

  # see #427
  it "cleanup when passed a proc", gc_dependent: true do
    #  NOTE: passing a proc is touchy, because it's so easy to create a memory leak.
    #
    #  specifically, if we made an inline call to
    #
    #      AutoPointerTestHelper.method(:release).to_proc
    #
    #  we'd have a reference to the pointer and it would
    #  never get GC'd.
    expect(AutoPointerTestHelper).to receive(:release).at_least(loop_count-wiggle_room).times
    AutoPointerTestHelper.reset
    loop_count.times do
      FFI::AutoPointer.new(PointerTestLib.ptr_from_address(magic),
                           AutoPointerTestHelper.finalizer)
    end
    AutoPointerTestHelper.gc_everything loop_count
  end

  # see #427
  it "cleanup when passed a method", gc_dependent: true do
    expect(AutoPointerTestHelper).to receive(:release).at_least(loop_count-wiggle_room).times
    AutoPointerTestHelper.reset
    loop_count.times do
      FFI::AutoPointer.new(PointerTestLib.ptr_from_address(magic),
                           AutoPointerTestHelper.method(:release))
    end
    AutoPointerTestHelper.gc_everything loop_count
  end

  it "can be used as the return type of a function" do
    expect do
      Module.new do
        extend FFI::Library
        ffi_lib TestLibrary::PATH
        class CustomAutoPointer < FFI::AutoPointer
          def self.release(ptr); end
          private_class_method(:release)
        end
        attach_function :ptr_from_address, [ FFI::Platform::ADDRESS_SIZE == 32 ? :uint : :ulong_long ], CustomAutoPointer
      end
    end.not_to raise_error
  end

  describe "#new" do
    it "MemoryPointer argument raises TypeError" do
      expect { FFI::AutoPointer.new(FFI::MemoryPointer.new(:int))}.to raise_error(::TypeError)
    end
    it "AutoPointer argument raises TypeError" do
      expect { AutoPointerSubclass.new(AutoPointerSubclass.new(PointerTestLib.ptr_from_address(0))) }.to raise_error(::TypeError)
    end
    it "Buffer argument raises TypeError" do
      expect { FFI::AutoPointer.new(FFI::Buffer.new(:int))}.to raise_error(::TypeError)
    end

  end

  describe "#autorelease?" do
    ptr_class = Class.new(FFI::AutoPointer) do
      def self.release(ptr); end
      private_class_method(:release)
    end

    it "should be true by default" do
      expect(ptr_class.new(FFI::Pointer.new(0xdeadbeef)).autorelease?).to be true
    end

    it "should return false when autorelease=(false)" do
      ptr = ptr_class.new(FFI::Pointer.new(0xdeadbeef))
      ptr.autorelease = false
      expect(ptr.autorelease?).to be false
    end

    it "should deny changes when frozen" do
      ptr = ptr_class.new(FFI::Pointer.new(0xdeadbeef)).freeze
      expect{ ptr.autorelease = false }.to raise_error(FrozenError)
    end
  end

  describe "#type_size" do
    ptr_class = Class.new(FFI::AutoPointer) do
      def self.release(ptr); end
      private_class_method(:release)
    end

    it "type_size of AutoPointer should match wrapped Pointer" do
      aptr = ptr_class.new(FFI::Pointer.new(:int, 0xdeadbeef))
      expect(aptr.type_size).to eq(FFI.type_size(:int))
    end

    it "[] offset should match wrapped Pointer" do
      mptr = FFI::MemoryPointer.new(:int, 1024)
      aptr = ptr_class.new(FFI::Pointer.new(:int, mptr))
      aptr[0].write_uint(0xfee1dead)
      aptr[1].write_uint(0xcafebabe)
      expect(mptr[0].read_uint).to eq(0xfee1dead)
      expect(mptr[1].read_uint).to eq(0xcafebabe)
    end
  end

  it "has a memsize function", skip: RUBY_ENGINE != "ruby" do
    base_size = ObjectSpace.memsize_of(Object.new)

    pointer = FFI::Pointer.new(:int, 0xdeadbeef)
    size = ObjectSpace.memsize_of(pointer)
    expect(size).to be > base_size
  end
end

