##
# A Foreign Function Interface used to bind C libraries to ruby.

module Rubinius
  module FFI

    #  Specialised error classes
    class TypeError < RuntimeError; end

    class NotFoundError < RuntimeError; end

    class << self
      def add_typedef(current, add)
        if current.kind_of? Fixnum
          code = current
        else
          code = FFI::TypeDefs[current]
          raise TypeError, "Unable to resolve type '#{current}'" unless code
        end

        FFI::TypeDefs[add] = code
      end

      def find_type(name)
        code = FFI::TypeDefs[name]
        raise TypeError, "Unable to resolve type '#{name}'" unless code
        return code
      end

      ##
      # Given a +type+ as a number, indicate how many bytes that type
      # takes up on this platform.

      def type_size(type)
        Rubinius.primitive :nativefunction_type_size

        case type
        when Symbol
          return type_size(find_type(type))
        when Rubinius::NativeFunction
          return type_size(TYPE_PTR)
        when FFI::Enum
          return type_size(TYPE_ENUM)
        end

        raise PrimitiveFailure, "FFI.type_size primitive failed: #{type}"
      end

      def size_to_type(size)
        if sz = TypeSizes[size]
          return sz
        end

        # Be like C, use int as the default type size.
        return :int
      end

      def config(name)
        Rubinius::Config["rbx.platform.#{name}"]
      end

      def config_hash(name)
        vals = {}
        section = "rbx.platform.#{name}."
        Rubinius::Config.section section do |key, value|
          vals[key.substring(section.size, key.length)] = value
        end
        vals
      end

      def errno
        FFI::Platform::POSIX.errno
      end

      # Convenience method for determining if a function call succeeded or failed
      def call_failed?(return_code)
        return_code == -1
      end
    end

    # Represents a C enum.
    class Enum
      attr_reader :tag
      attr_reader :kv_map

      def initialize(info, tag=nil)
        @tag = tag
        @kv_map = Hash.new

        unless info.nil?
          last_cst = nil
          value = 0
          info.each do |i|
            case i
            when Symbol
              @kv_map[i] = value
              last_cst = i
              value += 1
            when Integer
              @kv_map[last_cst] = i
              value = i + 1
            end
          end
        end
      end

      def inspect
        "#<%s:0x%x %s=>%s>" % [self.class, self.object_id, @tag, @kv_map.inspect]
      end

      def anonym?
        @tag.nil?
      end

      def values
        @kv_map.values
      end

      def symbols
        @kv_map.keys
      end

      def [](sym)
        @kv_map[sym]
      end

      def symbol(value)
        key, val = @kv_map.detect { |key, val| val == value }
        key
      end
    end

    # It's a class to be compat with the ffi gem.
    class Type
      class Array
        def initialize(element_type, size, impl_class=nil)
          @element_type = element_type
          @size = size
          @implementation = impl_class
        end

        attr_reader :element_type
        attr_reader :size
        attr_reader :implementation
      end

      class StructByValue
        def initialize(struct)
          @implementation = struct
        end

        attr_reader :implementation
      end

      Struct = StructByValue

      CHAR    = TYPE_CHAR
      UCHAR   = TYPE_UCHAR
      BOOL    = TYPE_BOOL
      SHORT   = TYPE_SHORT
      USHORT  = TYPE_USHORT
      INT     = TYPE_INT
      UINT    = TYPE_UINT
      LONG    = TYPE_LONG
      ULONG   = TYPE_ULONG
      LL      = TYPE_LL
      ULL     = TYPE_ULL
      FLOAT   = TYPE_FLOAT
      DOUBLE  = TYPE_DOUBLE
      PTR     = TYPE_PTR
      VOID    = TYPE_VOID
      STRING  = TYPE_STRING
      STRPTR  = TYPE_STRPTR
      CHARARR = TYPE_CHARARR
      ENUM    = TYPE_ENUM
      VARARGS = TYPE_VARARGS
    end

    ##
    # Namespace for holding platform-specific C constants.

    module Platform
      def self.windows?
        Rubinius.windows?
      end

      def self.mac?
        Rubinius.darwin?
      end

      def self.unix?
        ! windows?
      end
    end

    ##
    # Represents a C struct as ruby class.

    class Struct
      class InlineArray
        include Enumerable

        def initialize(type, ptr)
          @pointer = ptr
          @type = type.element_type
          @size = type.size
        end

        attr_reader :size

        def [](idx)
          if idx >= @size
            raise ArgumentError, "index out of range (#{idx} >= #{@size})"
          end

          @pointer.get_at_offset(idx * @type, @type)
        end

        def []=(idx, val)
          if idx >= @size
            raise ArgumentError, "index out of range (#{idx} >= #{@size})"
          end

          @pointer.set_at_offset(idx * @type, @type, val)
        end

        def each
          @size.times do |ele|
            yield @pointer.get_at_offset(ele * @type, @type)
          end
        end

        def to_a
          ary = []
          each { |x| ary << x }
          ary
        end

        def to_ptr
          @pointer
        end
      end

      class InlineCharArray < InlineArray
        def to_s
          str = @pointer.read_string(@size)

          # NULL clamp this, to be the same as the ffi gem.
          if idx = str.index("\0")
            return str[0, idx]
          end

          return str
        end

        alias_method :to_str, :to_s
        alias_method :inspect, :to_s
      end

      def self.find_nested_parent
        return nil if self.name.nil?

        path = self.name.split("::")
        path.pop # remove ourself

        mod = Object

        begin
          path.each { |c| mod = mod.const_get(c) }
        rescue NameError
          # bail.
          return nil
        end

        mod.respond_to?(:find_type) ? mod : nil
      end

      attr_reader :pointer

      def self.layout(*spec)
        return @layout if spec.size == 0

        # Pick up a enclosing FFI::Library
        @enclosing_module ||= find_nested_parent

        cspec = Rubinius::LookupTable.new
        i = 0

        @size = 0
        @members = []

        while i < spec.size
          name = spec[i]
          @members << name

          f = spec[i + 1]

          type_code = nil

          if f.kind_of? Array
            ary_type = f[0]
            ary_size = f[1]

            if @enclosing_module
              type_code = @enclosing_module.find_type(ary_type)
            end

            type_code ||= FFI.find_type(ary_type)
            type_size = FFI.type_size(type_code)

            case type_code
            when TYPE_CHAR, TYPE_UCHAR
              klass = InlineCharArray
            else
              klass = InlineArray
            end

            type = FFI::Type::Array.new(type_code, ary_size, klass)
            element_size = type_size * ary_size
          elsif f.kind_of?(Class) and (f < FFI::Struct || f < FFI::Union)
            type = FFI::Type::StructByValue.new(f)
            element_size = type_size = f.size
          else
            if @enclosing_module
              type_code = @enclosing_module.find_type(f) rescue nil
            end

            type_code ||= FFI.find_type(f)

            type = type_code
            element_size = type_size = FFI.type_size(type_code)
          end

          offset = spec[i + 2]

          if offset.kind_of?(Fixnum)
            i += 3
          else
            if self < FFI::Union
              offset = 0
            else
              offset = @size

              mod = offset % type_size
              unless mod == 0
                # we need to align it.
                offset += (type_size - mod)
              end
            end

            i += 2
          end

          cspec[name] = [offset, type]
          ending = offset + element_size
          @size = ending if @size < ending
        end

        @layout = cspec unless self == FFI::Struct

        return cspec
      end

      def self.config(base, *fields)
        @size = Rubinius::Config["#{base}.sizeof"]
        cspec = Rubinius::LookupTable.new

        fields.each do |field|
          offset         = Rubinius::Config["#{base}.#{field}.offset"]
          element_size   = Rubinius::Config["#{base}.#{field}.size"]
          type           = Rubinius::Config["#{base}.#{field}.type"]
          type           = type ? type.to_sym : FFI.size_to_type(size)

          type_code = FFI.find_type type

          if :char_array == type
            type_code = FFI::Type::Array.new(FFI.find_type(:char), element_size, InlineCharArray)
          end

          cspec[field] = [offset, type_code]
          ending = offset + element_size
          @size = ending if @size < ending
        end

        @layout = cspec

        return cspec
      end

      def self.size
        @size
      end

      def size
        self.class.size
      end

      def self.offset_of(name)
        @layout[name].first
      end

      def offset_of(name)
        @cspec[name].first
      end

      def self.offsets
        members.map do |member|
          [member, @layout[member].first]
        end
      end

      def offsets
        members.map do |member|
          [member, @cspec[member].first]
        end
      end

      def self.members
        @members
      end

      def members
        self.class.members
      end

      def initialize(pointer=nil, *spec)
        @cspec = self.class.layout(*spec)

        if pointer
          @pointer = pointer
        else
          @pointer = MemoryPointer.new size
        end
      end

      def to_ptr
        @pointer
      end

      def free
        @pointer.free
      end

      def ==(other)
        return false unless other.is_a?(self.class)
        @pointer == other.pointer
      end

      def initialize_copy(ptr)
        @pointer = ptr.pointer.dup
      end

      def []=(field, val)
        offset, type = @cspec[field]
        raise "Unknown field #{field}" unless offset

        case type
        when Fixnum
          @pointer.set_at_offset(offset, type, val)
        when FFI::Type::Array
          if type.implementation == InlineCharArray
            (@pointer + offset).write_string StringValue(val), type.size
            return val
          end

          raise TypeError, "Unable to set inline array"
        when Rubinius::NativeFunction
          @pointer.set_at_offset(offset, FFI::TYPE_PTR, val)
        else
          @pointer.set_at_offset(offset, type, val)
        end

        return val
      end

      def [](field)
        offset, type = @cspec[field]
        raise "Unknown field #{field}" unless offset

        case type
        when FFI::TYPE_CHARARR
          (@pointer + offset).read_string
        when Fixnum
          @pointer.get_at_offset(offset, type)
        when FFI::Type::Array
          type.implementation.new(type, @pointer + offset)
        when FFI::Type::StructByValue
          type.implementation.new(@pointer + offset)
        when Rubinius::NativeFunction
          ptr = @pointer.get_at_offset(offset, FFI::TYPE_PTR)
          if ptr
            FFI::Function.new(type.return_type, type.argument_types, ptr)
          else
            nil
          end
        else
          @pointer.get_at_offset(offset, type)
        end
      end

      def values
        members.map { |m| self[m] }
      end

      def null?
        @pointer == FFI::Pointer::NULL
      end
    end

    ##
    # Represents a C union as ruby class.

    class Union < Struct
    end
  end
end
