# -*- ruby -*-

require 'dl'
require 'dl/types'

module DL
  module Importable
    LIB_MAP = {}

    module Internal
      def init_types()
	@types ||= ::DL::Types.new
      end

      def init_sym()
	@SYM ||= {}
      end

      def [](name)
	return @SYM[name.to_s][0]
      end

      def dlload(*libnames)
	if( !defined?(@LIBS) )
	  @LIBS = []
	end
	libnames.each{|libname|
	  if( !LIB_MAP[libname] )
	    LIB_MAP[libname] = DL.dlopen(libname)
	  end
	  @LIBS.push(LIB_MAP[libname])
	}
      end
      alias dllink :dlload

      def parse_cproto(proto)
	proto = proto.gsub(/\s+/, " ").strip
	case proto
	when /^([\d\w\*_\s]+)\(([\d\w\*_\s\,\[\]]*)\)$/
	  ret = $1
	  args = $2
	  ret = ret.split(/\s+/)
	  args = args.split(/\s*,\s*/)
	  func = ret.pop
	  if( func =~ /^\*/ )
	    func.gsub!(/^\*+/,"")
	    ret.push("*")
	  end
	  ret  = ret.join(" ")
	  return [func, ret, args]
	else
	  raise(RuntimeError,"can't parse the function prototype: #{proto}")
	end
      end

      # example:
      #   extern "int strlen(char*)"
      #
      def extern(proto)
	func,ret,args = parse_cproto(proto)
	return import(func, ret, args)
      end

      # example:
      #   callback "int method_name(int, char*)"
      #
      def callback(proto)
	func,ret,args = parse_cproto(proto)

	init_types()
	init_sym()

	rty,renc,rdec = @types.encode_return_type(ret)
        if( !rty )
          raise(TypeError, "unsupported type: #{ret}")
        end
	ty,enc,dec = encode_argument_types(args)
	symty = rty + ty

	module_eval("module_function :#{func}")
	sym = module_eval([
	  "DL::callback(\"#{symty}\"){|*args|",
	  "  sym,rdec,enc,dec  = @SYM['#{func}']",
	  "  args = enc.call(args) if enc",
	  "  r,rs = #{func}(*args)",
	  "  r  = renc.call(r) if rdec",
	  "  rs = dec.call(rs) if (dec && rs)",
	  "  @retval = r",
	  "  @args   = rs",
	  "  @retval",
	  "}",
	].join("\n"))

	@SYM[func] = [sym,rdec,enc,dec]

	return sym
      end

      # example:
      #  typealias("uint", "unsigned int")
      #
      def typealias(*args)
	init_types()
	@types.typealias(*args)
      end

      # example:
      #  symbol "foo_value"
      #  symbol "foo_func", "IIP"
      #
      def symbol(name, ty = nil)
	sym = nil
	@LIBS.each{|lib|
	  begin
	    if( ty )
	      sym = lib[name, ty]
	    else
	      sym = lib[name]
	    end
	  rescue
	    next
	  end
	}
	if( !sym )
	  raise(RuntimeError, "can't find the symbol `#{name}'")
	end
	return sym
      end

      # example:
      #   import("get_length", "int", ["void*", "int"])
      #
      def import(name, rettype, argtypes = nil)
	init_types()
	init_sym()

	rty,_,rdec = @types.encode_return_type(rettype)
        if( !rty )
          raise(TypeError, "unsupported type: #{rettype}")
        end
	ty,enc,dec = encode_argument_types(argtypes)
	symty = rty + ty

	sym = symbol(name, symty)

	mname = name.dup
	if( ?A <= mname[0] && mname[0] <= ?Z )
	  mname[0,1] = mname[0,1].downcase
	end
	@SYM[mname] = [sym,rdec,enc,dec]
	
	module_eval [
	  "def #{mname}(*args)",
	  "  sym,rdec,enc,dec  = @SYM['#{mname}']",
	  "  args = enc.call(args) if enc",
	  if( $DEBUG )
	    "  p \"[DL] call #{mname} with \#{args.inspect}\""
	  else
	    ""
	  end,
	  "  r,rs = sym.call(*args)",
	  if( $DEBUG )
	    "  p \"[DL] retval=\#{r.inspect} args=\#{rs.inspect}\""
	  else
	    ""
	  end,
	  "  r  = rdec.call(r) if rdec",
	  "  rs = dec.call(rs) if dec",
	  "  @retval = r",
	  "  @args   = rs",
	  "  return @retval",
	  "end",
	  "module_function :#{mname}",
	].join("\n")

	return sym
      end

      def _args_
	return @args
      end

      def _retval_
	return @retval
      end

      def encode_argument_types(tys)
	init_types()
	encty = []
	enc = nil
	dec = nil
	tys.each_with_index{|ty,idx|
	  ty,c1,c2 = @types.encode_argument_type(ty)
          if( !ty )
            raise(TypeError, "unsupported type: #{ty}")
          end
	  encty.push(ty)
	  if( enc )
	    if( c1 )
	      conv1 = enc
	      enc = proc{|v| v = conv1.call(v); v[idx] = c1.call(v[idx]); v}
	    end
	  else
	    if( c1 )
	      enc = proc{|v| v[idx] = c1.call(v[idx]); v}
	    end
	  end
	  if( dec )
	    if( c2 )
	      conv2 = dec
	      dec = proc{|v| v = conv2.call(v); v[idx] = c2.call(v[idx]); v}
	    end
	  else
	    if( c2 )
	      dec = proc{|v| v[idx] = c2.call(v[idx]); v}
	    end
	  end
	}
	return [encty.join, enc, dec]
      end
    end # end of Internal
    include Internal
  end # end of Importable
end
