require 'rubygems'
require 'benchmark'
require 'iconv'
require 'ffi'

page = "test\321test" * 100000
page2 = "test test"   * 100000

module FFIIconv
  # BUFFER_SIZE = 8_192 * 4
  class IconvError < StandardError; end

  module LibIconv
    extend FFI::Library
    ffi_lib 'libiconv', 'libm'

    attach_function :iconv_open, [:string, :string], :pointer
    attach_function :iconv, [:pointer, :pointer, :pointer, :pointer, :pointer], :long
    attach_function :iconv_close, [:pointer], :int
  end

  class << self
    def reset_outptr(outbuf, outptr, outlength, str_len)
      outptr.write_pointer(outbuf)
      outlength.write_long(str_len)
    end

    def with_buffers(str, str_len)
      inbuf = FFI::MemoryPointer.from_string(str)
      outbuf = FFI::MemoryPointer.new(:char, str_len)

      inptr  = FFI::MemoryPointer.new(:pointer)
      outptr = FFI::MemoryPointer.new(:pointer)

      inlength  = FFI::MemoryPointer.new(:size_t)
      outlength = FFI::MemoryPointer.new(:size_t)

      inptr.write_pointer(inbuf)
      inlength.write_long(str.length)
      reset_outptr(outbuf, outptr, outlength, str_len)

      yield [outbuf, inptr, outptr, inlength, outlength]
    ensure
      inbuf.free if inbuf
      outbuf.free if outbuf
      inptr.free if inptr
      outptr.free if outptr
      inlength.free if inlength
      outlength.free if outlength
    end

    def conv(to, from, str)
      iconv = LibIconv.iconv_open(to, from)

      if iconv == FFI::Pointer.new(-1)
        iconv = nil
        raise IconvError, "invalid encodings"
      end

      out = ''

      str_len = str.length
      in_bytes_left = str.length

      with_buffers(str, str_len) do |outbuf, inptr, outptr, inlength, outlength|
        while in_bytes_left > 0
          res = LibIconv.iconv(iconv, inptr, inlength, outptr, outlength)
          in_bytes_left = inlength.read_long
          if res == -1
            errno = FFI.errno
            if errno  == Errno::EILSEQ::Errno || errno == Errno::EINVAL::Errno
              inptr.write_pointer(inptr.read_pointer + 1)
              inlength.write_long(in_bytes_left - 1)
            end
          end
          output_bytes_left = outlength.read_long
          if output_bytes_left < 100 || in_bytes_left == 0
            out << outbuf.read_string(str_len - output_bytes_left)
            reset_outptr(outbuf, outptr, outlength, str_len)
          end
        end
      end

      out
    ensure
      LibIconv.iconv_close(iconv) if iconv
    end
  end
end


def a(page)
  out = ""
  Iconv.open("utf-8", "utf-8") do |iconv|
    begin
      out << iconv.iconv(page)
    rescue Iconv::IllegalSequence, Iconv::InvalidCharacter => e
      out << e.success
      page = e.failed[1..-1]
      retry
    end
  end
  out
end


# FFIIconv.conv('dfadsfa', 'fadsfas', page)
raise 'fail' unless FFIIconv.conv('utf-8', 'utf-8', page) == a(page)
raise 'fail' unless FFIIconv.conv('utf-8', 'utf-8', page2) == a(page2)

Benchmark.bm do |bm|
  GC.start
  bm.report('rescue. incorrect utf8') { a(page) }
  GC.start
  bm.report('rescue. correct utf8') { a(page2) }
  GC.start
  bm.report('ffi. incorrect utf8') { FFIIconv.conv('utf-8', 'utf-8', page) }
  GC.start
  bm.report('ffi. correct utf8') { FFIIconv.conv('utf-8', 'utf-8', page2) }
end

output

     user     system      total        real
rescue. incorrect utf8  1.780000   0.020000   1.800000 (  1.992797)
rescue. correct utf8  0.010000   0.000000   0.010000 (  0.016532)
ffi. incorrect utf8  0.520000   0.000000   0.520000 (  0.576972)
ffi. correct utf8  0.010000   0.000000   0.010000 (  0.009533)