require 'natalie/inline'

__inline__ '#include <dlfcn.h>'
__ld_flags__ '-ldl'

class Fiddle
  class DLError
  end

  TYPE_VOID = :void
  TYPE_VOIDP = :voidp

  class << self
    def dlopen(path) = Handle.new(path)
  end

  class Handle
    __define_method__ :initialize, [:path], <<-END
      path.assert_type(env, Object::Type::String, "String");
      auto handle = dlopen(path.as_string()->c_str(), RTLD_LAZY);
      if (!handle) {
          auto dl_error = self.klass()->const_find(env, "DLError"_s, Object::ConstLookupSearchMode::NotStrict).value().as_class();
          env->raise(dl_error, "{}", dlerror());
      }
      auto handle_ptr = VoidPObject::create(handle);
      self->ivar_set(env, "@ptr"_s, handle_ptr);
      return self;
    END

    __define_method__ :[], [:name], <<-END
      auto handle = self->ivar_get(env, "@ptr"_s).as_void_p()->void_ptr();
      name.assert_type(env, Object::Type::String, "String");
      auto symbol = dlsym(handle, name.as_string()->c_str());
      return Value::integer((long long)symbol);
    END
  end

  class Pointer
    __define_method__ :to_s, <<-END
      auto len = args.at(0, Value::nil());
      if (!len.is_nil())
        len.assert_integer(env);
      auto ptr_obj = self->ivar_get(env, "@ptr"_s);
      ptr_obj.assert_type(env, Object::Type::VoidP, "VoidP");
      auto ptr = (const char *)ptr_obj.as_void_p()->void_ptr();
      if (!len.is_nil())
        return StringObject::create(ptr, (size_t)len.integer().to_nat_int_t());
      return StringObject::create(ptr);
    END
  end

  class Function
    def initialize(symbol, arg_types, return_type)
      @symbol = symbol
      @arg_types = arg_types
      @return_type = return_type
    end

    def call(*args)
      if args.size != @arg_types.size
        raise ArgumentError, "wrong number of arguments (given #{args.size}, expected #{@arg_types.size})"
      end
      internal_method_name = "#{@return_type}_"
      if @arg_types.empty?
        internal_method_name += 'no_args'
      else
        internal_method_name += 'args_'
        internal_method_name += @arg_types.join('_')
      end
      if defined?(internal_method_name)
        send(internal_method_name, *args)
      else
        raise NoMethodError, "Fiddle: no method with name: #{internal_method_name}"
      end
    end

    private

    # NOTE: currently only supporting the few function signatures that we need for Natalie's REPL
    # In the future, Natalie will have macros that will allow this to be generated at compile time.

    __define_method__ :void_no_args, [], <<-END
      auto symbol = self->ivar_get(env, "@symbol"_s).integer().to_nat_int_t();
      auto fn = (void* (*)())symbol;
      fn();
      return Value::nil();
    END

    __define_method__ :voidp_no_args, [], <<-END
      auto symbol = self->ivar_get(env, "@symbol"_s).integer().to_nat_int_t();
      auto fn = (void* (*)())symbol;
      auto result = fn();
      auto pointer_class = self.klass()->const_find(env, "Pointer"_s, Object::ConstLookupSearchMode::NotStrict).value().as_class();
      auto pointer_obj = Object::create(Object::Type::Object, pointer_class);
      auto pointer_ptr = VoidPObject::create(result);
      pointer_obj->ivar_set(env, "@ptr"_s, pointer_ptr);
      return pointer_obj;
    END

    __define_method__ :voidp_args_voidp, [:p1], <<-END
      auto symbol = self->ivar_get(env, "@symbol"_s).integer().to_nat_int_t();
      auto fn = (void* (*)(void*))symbol;
      void *p1_ptr;
      auto pointer_class = self.klass()->const_find(env, "Pointer"_s, Object::ConstLookupSearchMode::NotStrict).value().as_class();
      if (p1.is_a(env, pointer_class))
          p1_ptr = p1->ivar_get(env, "@ptr"_s).as_void_p()->void_ptr();
      else if (p1.is_void_p())
          p1_ptr = p1.as_void_p()->void_ptr();
      else
          p1_ptr = (void*)(p1.object());
      auto result = fn(p1_ptr);
      auto pointer_obj = Object::create(Object::Type::Object, pointer_class);
      auto pointer_ptr = VoidPObject::create(result);
      pointer_obj->ivar_set(env, "@ptr"_s, pointer_ptr);
      return pointer_obj;
    END
  end
end
