#!/usr/bin/env ruby
require 'optparse'
################################################################
# CONFIGURATION:
CFTypeInfo             = {
  'int'                  => {:namedConst       => 'c_int'                , :ftype => 'integer'},
  'short int'            => {:namedConst       => 'c_short'              , :ftype => 'integer'},
  'long int'             => {:namedConst       => 'c_long'               , :ftype => 'integer'},
  'long long int'        => {:namedConst       => 'c_long_long'          , :ftype => 'integer'},
  'signed char'          => {:namedConst       => 'c_signed_char'        , :ftype => 'integer'},
  'unsigned char'        => {:namedConst       => 'c_signed_char'        , :ftype => 'integer'},
  'size_t'               => {:namedConst       => 'c_size_t'             , :ftype => 'integer'},
  'int8_t'               => {:namedConst       => 'c_int8_t'             , :ftype => 'integer'},
  'int16_t'              => {:namedConst       => 'c_int16_t'            , :ftype => 'integer'},
  'int32_t'              => {:namedConst       => 'c_int32_t'            , :ftype => 'integer'},
  'int64_t'              => {:namedConst       => 'c_int64_t'            , :ftype => 'integer'},
  'int_fast8_t'          => {:namedConst       => 'c_int_fast8_t'        , :ftype => 'integer'},
  'int_fast16_t'         => {:namedConst       => 'c_int_fast16_t'       , :ftype => 'integer'},
  'int_fast32_t'         => {:namedConst       => 'c_int_fast32_t'       , :ftype => 'integer'},
  'int_fast64_t'         => {:namedConst       => 'c_int_fast64_t'       , :ftype => 'integer'},
  'int_least8_t'         => {:namedConst       => 'c_int_least8_t'       , :ftype => 'integer'},
  'int_least16_t'        => {:namedConst       => 'c_int_least16_t'      , :ftype => 'integer'},
  'int_least32_t'        => {:namedConst       => 'c_int_least32_t'      , :ftype => 'integer'},
  'int_least64_t'        => {:namedConst       => 'c_int_least64_t'      , :ftype => 'integer'},
  'intmax_t'             => {:namedConst       => 'c_intmax_t'           , :ftype => 'integer'},
  'intptr_t'             => {:namedConst       => 'c_intptr_t'           , :ftype => 'integer'},

  'float'                => {:namedConst       => 'c_float'              , :ftype => 'real'},
  'double'               => {:namedConst       => 'c_double'             , :ftype => 'real'},
  'long double'          => {:namedConst       => 'c_long_double'        , :ftype => 'real'},

  'float _Complex'       => {:namedConst       => 'c_float_complex'      , :ftype => 'complex'},
  'double _Complex'      => {:namedConst       => 'c_double_complex'     , :ftype => 'complex'},
  'long double _Complex' => {:namedConst       => 'c_long_double_complex', :ftype => 'complex'},
  '_Bool'                => {:namedConst       => 'c_bool'               , :ftype => 'logical'},
  'char'                 => {:namedConst       => 'c_char'               , :ftype => 'character'}
}
# how the module should be invoked from fortran
ModuleName    = 'mo_cdi'
# which conversion is to use generating the fortran routine names, this could
# be any ruby String method.
FNameMethod   = :noop
# FNameMethod = :downcase
# FNameMethod = :noop
# FNameMethod = :upcase
class String;def noop;self;end;end
# fortran subroutines are not allowed to have a parameters with the same name,
# so in case of a match, these parameters have to get an new name
FParamExtension = 'v'
# Naming convention from above: 
# all non scalar variables should have the postfix '_vec' in there name
Vectors = /_vec$/i
################################################################################
FortranMaxLineLength = 132
# read the c header file and grep out name, return type and paramterlist of
# each function
def getFuncInfo(filename)
  typelist = %w[char int float double void]
  cppflags = ENV['CPPFLAGS'].nil? ? '' : ENV['CPPFLAGS']
  funclist = IO.popen("cpp #{cppflags} #{filename} | cpp -fpreprocessed").readlines.delete_if {|line| line.include?('#')}.collect {|line| line.chomp}
  # delete everything, that do not look like a function prototype
  typeRegexp = /^.*(#{typelist.join('|')}) \**\w+\s*\(.*\)/
  funclist.delete_if {|line|
    not typeRegexp.match(line.lstrip)
  }
  funclist.collect! {|line|
    md = /(\w+)+ +(\**)(\w+)\s*\((.*)\)/.match(line)
    returnType, returnPointer, funcName, paramList = md[1,4]
    paramList = paramList.split(',').collect {|p| p.split(' ').each {|_p| _p.strip}}
    [funcName, returnType, returnPointer, paramList]
  }
  funclist
end

# grep the #define C-Constants, which should be available within the fortran CDI API
def getDefines(filename)
  defines = File.open(filename,'r').readlines.grep(/^#define/).collect {|line|
    md = / +(\w+) +(-*\d+)/.match(line)
  }.select {|item| not item.nil?}.collect {|match| match[1..2]}
end

# create continuation for lines longer that 132 sign, which would create an
# error with some fortran compilers
def genContinuation(iline)
  # leave the input untouched
  line = iline
  # try to create readable line breaks, i.e. do not split name, labels or keywords
  regexp = /,[^,]*$/

  matchIndex = line[0,FortranMaxLineLength] =~ regexp

  if matchIndex.nil?
    line[FortranMaxLineLength-2] = "&\n"+line[FortranMaxLineLength-2,1]+"&"
  else
    line[matchIndex] = ",&\n" + ' '*7
  end
  line
end


# create fortran module variables
def genModParams(vars)
  vars.collect {|var, value|
    "      integer, parameter :: #{var} = #{value}"
  }.join("\n")
end

# return the fortran version of the c parameters (name, named constant within
# iso-c-bindig, fortran type)
def fortranParamsWithTypes(paramList)
  paramList.collect {|paramInfo|
    ctype, param = paramInfo[-2,2]
    ftype, nc    = CFTypeInfo[ctype][:ftype], CFTypeInfo[ctype][:namedConst]
    # fortran parameters can be configured out of the c parameters
    [param.send(FNameMethod),nc,ftype, paramInfo.include?('const')]
  }
end
def startMod(name)
  "
module #{name}
      use, intrinsic :: iso_c_binding

      implicit none

      private
  "
end
def endMod(name)
  "\nend module #{name}\n"
end
def isBadFunction(returnType, returnPointer, paramList)
  return true if (
    # external return type
    (returnType != 'void' and not CFTypeInfo.keys.include?(returnType)) or
    # pointer2pointer return type
    returnPointer.length > 1
  )
  paramList.each {|paramInfo|
    next if paramInfo == ['void']
    next if paramInfo[0] == 'void' and /^\*\w+$/.match(paramInfo[1])
    ctype, param = paramInfo[-2,2]
    # TJ: unnamed arguments shouldn't be matched at all but because
    # pointer * is parsed as part of the name those need to be rejected here
    return true if (
      CFTypeInfo[ctype].nil? or                        # derived data type
      param == '*' or                                  # unnamed pointer
      param[0,2] == '**' or                            # pointer2pointer
      (param[0,1] == '*' and /\w\[\w*\]/.match(param)) # array of pointers
    )
  }
  return false
end

def hasDimension(paramName, paramFType)
  return true if paramFType == 'character'
  return true if ( %w[integer real].include?(paramFType) and Vectors.match(paramName) )
  return false
end

# collect further information about the original c type of the params and
# in case of a match between param name and function name, the parameter name
# is changed
def setFortranParams(paramswithtypes,fFuncname)
  # special treatment of empty parameter list
  return [[],[]] if paramswithtypes == [[]]

  originalParameters = paramswithtypes.transpose[0]
  paramswithtypes.collect {|param, paramCType, fType, isConstant|
    # test for pointers/arrays
    isPointer, isArray, arraySize = false, false, nil
    if param[0,1] == '*'
      isPointer = true
      # remove '*' from funcnames and paramnames
      param.sub!('*','')
    end
    if ( md = /\w\[(\w*)\]/.match(param); not md.nil? )
      isArray   = true
      arraySize = md[1] == '' ? '*' : md[1]
      param[md.begin(1)-1,md.end(1)-1] = ''
    end

    # change param name if it equals the funcname
    if param == fFuncname or param.downcase == fFuncname.downcase
      param += FParamExtension
      # but maybe the result is the name of another parameter. -> play it again sam...
      param += FParamExtension while ( originalParameters.include?(param) )
    end
    if param[0,1] == '_'
      param = 'p' + param
    end

    [param,paramCType,fType,isPointer,isArray,arraySize,isConstant]
  }
end

def printParams(fParams, indent)
  out = ''
  fParams.each {|param,paramType,ftype,ispointer,isarray,arraysize,isconstant|
    dimension     = isarray ? "dimension(#{arraysize})" : ( (ispointer and hasDimension(param, ftype) ) ? 'dimension(*)' : nil)
    intent, value = nil, nil
    if (ispointer or isarray)
      if not isconstant
        intent = 'intent(out)'
      else
        intent = 'intent(in)'
      end unless paramType == 'c_char'
    else
      #intent = 'intent(in)'
      value  = 'value'
    end

    typeinfo = [value,intent,dimension].select {|s| ! s.nil?}.join(', ')
    out << "  #{indent}#{ftype}"
    out << (paramType == 'c_ptr' ? '' : "(kind=#{paramType})")
    out << ", #{typeinfo} :: #{param}\n"
  }
  return out
end

# creates the actual binding within the module for the given c function
# unsupported types of function a ignored, see RESTRICTIONS (top) for details
def genInterface(cFuncname, returnType, returnPointer, paramList, debug)

  # do not create interfaces for unsupported functions
  if isBadFunction( returnType, returnPointer, paramList)
    warn "parameterlist of '#{cFuncname}' is not supported -> function ignored."
    return ['','']
  end
  return ['', ''] if (cFuncname[0,1] == '_')
  # the void argument type can be left out: if 'void' occurs in the
  # parameterlist (which is only the case, if it is the only parameter
  # information), it is simply removed and a empty paramter list is left.
  paramList = [[],[]] if paramList.flatten == [ 'void' ] or paramList.flatten.empty?

  out = ''
  isWrapper = false

  # create new names for the fortran routines, see CONFIGURATION (top)
  fFuncname = cFuncname.send(FNameMethod)

  paramsWithTypes = []
  # divide between empty and non empty parameter lists
  if paramList == [[],[]]
    fParams, fParams4Import, fTypes4Import = [], [],[]
  else
    # collect information for setting the correct fortran type for each parameter
    paramsWithTypes = paramList.collect {|paramInfo|
      ctype, param = paramInfo[-2,2]
      integral_type = /^(:?char|int|long|short)$/.match(ctype);
      if (integral_type)
        while (/^(:?un)?signed/.match(paramInfo[-3]))
          ctype = paramInfo[-3] << ' ' << ctype
          paramInfo.delete_at(-3)
          paramInfo[-2] = ctype
          pp [paramInfo] if debug
        end
      end
      ptr_match = /^\*(\w+)$/.match(param)
      if (/^(:?const)? *void$/.match(ctype) and ptr_match)
        param = ptr_match[1]
        ftype = 'type(c_ptr)'
        nc = 'c_ptr'
      else
        ftype, nc    = CFTypeInfo[ctype][:ftype], CFTypeInfo[ctype][:namedConst]
      end
      # fortran parameters can be configured out of the c parameters
      [param.send(FNameMethod),nc,ftype, paramInfo.include?('const')]
    }

    # get deeper information about parameterlists and filter out functions with
    # non supported parameters
    fParams = setFortranParams(paramsWithTypes,fFuncname)
    fParams4Import, fTypes4Import, = fParams.transpose
  end

  fReturnInfo                    = CFTypeInfo[returnType]
  indent = '      '
  if not fReturnInfo.nil?
    fReturnString = "function"
    fEnd    = ["#{fReturnInfo[:ftype]}(kind=#{fReturnInfo[:namedConst]}) :: #{fFuncname}\n", "end function" ]
    fTypes4Import << fReturnInfo[:namedConst]
  elsif returnType == 'void'
    fReturnString = "subroutine"
    fEnd    = ["end subroutine"]
  end
  fFuncname_suffix = ''

  if (returnType == 'char' and returnPointer == '*')
    out << "#{indent}function #{fFuncname}(#{fParams4Import.join(',')})\n"
    out << printParams(fParams, indent)
    fTypes4Import.unshift('c_ptr') unless fTypes4Import.include?('c_ptr')
    isWrapper = true
    fwEnd = [ "end function" ]
    fFuncname_suffix = '_c'
    indent = '        '
    fEnd = [ "type(c_ptr) :: #{fFuncname}#{fFuncname_suffix}\n",
             "end function" ]
  end
  out << "#{indent}interface
#{indent}  #{fReturnString} #{fFuncname}#{fFuncname_suffix}(#{fParams4Import.join(',')}) bind(c,name='#{cFuncname}')\n"
  out << "#{indent}    import :: #{fTypes4Import.uniq.join(',')}\n" unless fTypes4Import.empty?

  out << printParams(fParams, indent + '  ')

  fEnd.each_with_index do |line, i|
    extra_indent = '    '
    if i == fEnd.length - 1
      extra_indent = '  '
    end
    out << indent + extra_indent + line
  end
  out << " #{fFuncname}#{fFuncname_suffix}\n#{indent}end interface\n"
  if (returnType == 'char' and returnPointer == '*')
    indent = '      '
    out << "#{indent}  character(len=1, kind=c_char), pointer :: #{fFuncname}(:)
#{indent}  type(c_ptr) :: cptr
#{indent}  integer :: slen(1)

#{indent}  cptr = #{fFuncname}#{fFuncname_suffix}("
    out << fParams4Import.join(",&
#{indent}    ") << ")\n"
    out << "#{indent}  #{fFuncname} => null()\n"
    out << "#{indent}  slen(1) = int(strlen(cptr))\n"
    out << "#{indent}  call c_f_pointer(cptr, #{fFuncname}, slen)\n"
    fwEnd.each_with_index do |line,i|
      extra_indent = '  '
      if i == fwEnd.length - 1
        extra_indent = ''
      end
      out << indent + extra_indent + line
    end
    out << " #{fFuncname}\n"
  end
  [out, makePublic(fFuncname), isWrapper]
end
def makePublic(*fFuncnameList)
  fFuncnameList.collect {|fname| 
    "      public :: #{fname.tr('*','')}"
  }.join("\n") << "\n"
end
def ctrim
  "
    subroutine ctrim(str)
    character(kind=c_char), intent(inout) :: str(:)
    character(kind=c_char) :: c
    integer :: i

    do i=1,size(str)
      c = str(i)
      if (c == c_null_char) then
        str(i:size(str)) = ' '
        exit
      end if
    end do

    end subroutine ctrim\n"
end

def clen
  "
    function c_len(s) result(i)
      character(kind=c_char), intent(in) :: s(:)
      integer :: i
      do i = 1, size(s)
        if (s(i) == c_null_char) then
          exit
        end if
      end do
      i = i - 1
    end function\n"
end

################################################################################
if __FILE__ == $0
require 'optparse'
require 'pp'

debug = false
OptionParser.new do |opts|
  opts.on("-d","--debug") {debug = true}
  opts.on_tail("--help","-h","-H","Display this help message.") do
    puts <<-'END'
#== Synopsis
# Create Fortran iso-c-bindings form a given c header file
# 
#== Usage
#   binGen.rb <headerFile> <fortranLibraryFile> [<modName>] [--help|--debug]  
#
# headerFile:
#   A general c header file: function prototypes and '#defines' with numerical
#   value will be taken for the fortran module construction. Furthermore there
#   are some restrictions to what is provided in fortran: Currently internal
#   datatypes and (arrays|pointers) to internal datatypes are supported, i.e.
#   no arrays of pointers, no pointer to pointers, no typedefs
#
# fortranLibraryFile:
#   file name for generated bindings
#
# modName:
#   This will be the name of the Fortran module, so it has to obey the fortran 
#   restriction for module names. default: mo_cdi
#  
#== Author
# Ralf Mueller, ralf.mueller@zmaw.de
#
#== RESTRICTIONS:
# ONLY SUPPORT FOR INTERNAL DATATYPES AND (ARRAYS|POINTERS) TO INTERNAL
# DATATYPES, I.E. NO ARRAYS OF POINTERS, NO POINTER TO POINTERS, NO TYPEDEFS
#
#=== Special: naming convention
# Pointers can have different sizes, which cannot be detetermined by parsing a
# function prototype. Therefor a convention according the parameter names can
# be used to take this decision precisely:
# * Pointers to numbers are expected to be scalars unless the name of the
#   parameter end with '_vec'
# * Pointers to char are allways referenced as vectors
#
#== LICENSE: 
# BSD License
#
#  Copyright (c) 2009-2012, Ralf Mueller (ralf.mueller@zmaw.de)
#  All rights reserved.
#  
#  Redistribution and use in source and binary forms, with or without
#  modification, are permitted provided that the following conditions are met:
#  
#      * Redistributions of source code must retain the above copyright notice,
#        this list of conditions and the following disclaimer.
#  
#      * Redistributions in binary form must reproduce the above copyright
#        notice, this list of conditions and the following disclaimer in the
#        documentation and/or other materials provided with the distribution.
#  
#      * The names of its contributors may not be used to endorse or promote
#        products derived from this software without specific prior written
#        permission.
#  
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
#  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
#  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
#  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
#  DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
#  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
#  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
#  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
#  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
END
  exit
  end
end.parse!

  if ARGV[1] == nil
    warn 'no outputile given'
    exit
  end

  outputString      = ''
  modname           = ARGV[2].nil? ? ModuleName : ARGV[2]

  cDefines          = getDefines(ARGV[0])
  pp cDefines if debug
  unless cDefines.empty?
    makeModVarsPublic = makePublic(*cDefines.transpose[0]) 
    moduleVariables   = genModParams(cDefines)
  end

  interfaces, makepublics, subroutines = '', '', "contains\n"
  indent = '    '

  funcdecls = [ [ 'strlen', 'size_t', '', [ [ 'void', '*s' ] ] ] ]
  funcdecls.concat(getFuncInfo(ARGV[0]))

  funcdecls.each {| funcName, returnType, returnPointer, paramList|
    pp [funcName, returnType, returnPointer, paramList] if debug
    interface, makepublic, isWrapper = genInterface(funcName,returnType, returnPointer, paramList, debug)
    if isWrapper
      subroutines << interface
    else
      interfaces  << interface
    end
    makepublics << makepublic
  }

  # add a specialized trim for wierd c strings
  makepublics << makePublic('ctrim') << makePublic('c_len')
  subroutines << ctrim << clen

  File.open(ARGV[1],"w") {|f|
    [ startMod(modname),
      moduleVariables ||= '',
      interfaces,
      makepublics,
      makeModVarsPublic ||= '',
      subroutines,
      endMod(modname)
    ].join("\n").split("\n").each {|line| 
      # check the length of each line before writing to file
      if line.length > FortranMaxLineLength
        f << genContinuation(line) << "\n"
      else
        f << line << "\n"
      end
    }
  }

end
