# genExtStubs.tcl --
#
#	Generates an import table for one or more external dynamic
#	link libraries.
#
# Usage:
#
#	tclsh genExtStubs.tcl stubDefs.txt stubStruct.h stubInit.c
#
# Parameters:
#
#	stubsDefs.txt --
#		Name of a file containing declarations of functions
#		to be stubbed. The functions are expected to be in
#		stylized C where exach appears on a single line, and
#		has the form 'returnType name(param,param,...);'
#		In addition, comments of the following forms
#		are expected to precede the function declarations.
#			/* LIBRARY: name1 name2... */
#		These comments give the rootnames of dynamic link
#		libraries that are expected to contain the functions,
#		in order of preference.
#			/* STUBSTRUCT: prefix */
#		String to be prepended to the function name that translates
#		to its reference in the stub table.
#	stubStruct.h --
#		Name of a file that will contain (a) the declaration
#		of a structure that contains pointers to the stubbed
#		functions, and (b) #defines replacing the function name
#		with references into the stub table

# parseImports --
#
#	Parse the import declarations in a given file
#
# Parameters:
#	stubDefs -- Name of the file to parse
#
# Results:
#
#	Returns a list of tuples. The possible tuples are:
#
#	    libraries NAME NAME...
#		Sets the names of the
#	    prefix NAME
#	        Sets the name of the stub structure to NAME and prefixes
#		all the definitions of the stubbed routines with NAME
#	    import TYPE NAME PARAMS
#		Declares the imported routine NAME to return data of type
#		TYPE and accept parmeters PARAMS.

proc parseImports {stubDefs} {

    set defsFile [open $stubDefs r]
    set imports {}
    set lineNo 0
    while {[gets $defsFile line] >= 0} {
	incr lineNo
	if {[string is space $line]} {
	    # do nothing
	} elseif {[regexp -expanded -- {
	    ^\s*\*\s*LIBRARY:\s+
	    ([a-zA-Z0-9_]+(?:\s+[a-zA-Z0-9_]+)*) # List of library names
	} $line -> m]} {
	    set libNames $m
	    lappend imports [linsert $libNames 0 libraries]
	} elseif {[regexp {^\s*\*\s*STUBSTRUCT:\s*(.*)} $line -> m]} {
	    set stubPrefix $m
	    lappend imports [list prefix $m]
	} elseif {[regexp {^\s*\*\s*CONVENTION:\s*(.*)} $line -> c]} {
	    lappend imports [list convention $c]
	} elseif {[regexp -nocase -- {^\s*#} $line]} {
	    # do nothing
	} elseif {[regexp -nocase -expanded -- {
	    \s*(.*)\s+			# Return type
	    ([[:alpha:]_][[:alnum:]_]+)	# Function name
	    \s*\((.*)\);		# Parameters
	} $line -> type name params]} {
	    lappend imports [list import $type $name $params]
	} else {
	    puts stderr "$stubDefs:$lineNo: unrecognized syntax"
	}
    }
    close $defsFile

    return $imports
}

# writeStructHeader --
#
#	Writes the header of the stubs structure to the '.h' file
#
# Parameters:
#	stubDefs   -- Name of the input file from which stubs are being
#		      generated
#	stubStruct -- Name of the file .h being written
#	structFile -- Channel ID of the .h file being written
#
# Results:
#	None.
#
# Side effects:
#	Writes the 'struct' header to the .h file

proc writeStructHeader {stubDefs stubStruct structFile} {

    chan puts $structFile "/*"
    chan puts $structFile " *[string repeat - 77]"
    chan puts $structFile " *"
    chan puts $structFile " * $stubStruct --"
    chan puts $structFile " *"
    chan puts $structFile " *\tStubs for procedures in [file tail $stubDefs]"
    chan puts $structFile " *"
    chan puts $structFile " * Generated by [file tail $::argv0]: DO NOT EDIT"
    chan puts $structFile " * [clock format [clock seconds] \
				-format {%Y-%m-%d %H:%M:%SZ} -gmt true]"
    chan puts $structFile " *"
    chan puts $structFile " *[string repeat - 77]"
    chan puts $structFile " */"
    chan puts $structFile ""
    chan puts $structFile "typedef struct [file rootname [file tail $stubDefs]] \{"

    return
}

# writeStubDeclarations --
#
#	Writes the declarations of the stubs in the table to the .h file.
#
# Parameters:
#	structFile -- Channel ID of the .h file
#	imports -- List of tuples returned from 'parseImports'
#
# Results:
#	None.
#
# Side effects:
#	C pointer-to-function declarations are written to the given file.

proc writeStubDeclarations {structFile imports} {

    set convention {}
    foreach i $imports {
	set key [lindex $i 0]
	switch -exact -- $key {
	    convention {
		set convention [lindex $i 1]
	    }
	    import {
		lassign $i key type name params
		chan puts $structFile \
		    "    $type (${convention}*${name}Ptr)($params);"
	    }
	    libraries {
		chan puts $structFile {}
		chan puts $structFile \
		    "    /* Functions from libraries: [lrange $i 1 end] */"
		chan puts $structFile {}
	    }
	    default {
	    }
	}
    }

    return
}

# writeStructFooter --
#
#	Writes the close of the 'struct' declaration to the .h file
#
# Parameters:
#	stubDefs   -- Name of the struct
#	structFile -- Channel handle of the .h file
#
# Results:
#	None
#
# Side effects:
#	Structure declaration is closed.

proc writeStructFooter {stubDefs structFile} {
    chan puts $structFile "\} [file rootname [file tail $stubDefs]]\;"
    return
}

# writeStubDefines --
#
#	Write the #define directives that replace stub function calls with
#	indirections through the stubs table.
#
# Parameters:
#	structFile -- Channel id of the .h file
#	imports    -- Table of imports from parseImports

proc writeStubDefines {structFile imports} {

    set stubPrefix {}
    foreach i $imports {
	switch -exact -- [lindex $i 0] {
	    prefix {
		lassign $i -> stubPrefix
	    }
	    import {
		lassign $i -> type name params
		chan puts $structFile "#define $name ($stubPrefix->${name}Ptr)"
	    }
	}
    }
    return $stubPrefix
}

# accumulateLibNames --
#
#	Accumulates the list of library names into the Stub initialization
#
# Parameters:
#	codeVar - Name of variable in caller's scope containing the code
#		  under construction
#	imports - Import definitions from 'parseImports'
#
# Results:
#	Returns the code burst for the initialization file.

proc accumulateLibNames {codeVar imports} {
    upvar 1 $codeVar code
    set sep "\n    "
    foreach i $imports {
	if {[lindex $i 0] eq {libraries}} {
	    foreach lib [lrange $i 1 end] {
		append code $sep \" $lib \"
		set sep ", "
	    }
	}
    }
    append code $sep "NULL"
}

# accumulateSymNames --
#
#	Accumulates the list of import symbols into the Stub initialization
#
# Parameters:
#	codeVar - Name of variable in caller's scope containing the code
#		  under construction
#	imports - Import definitions from 'parseImports'
#
# Results:
#	Returns the code burst for the initialization file.

proc accumulateSymNames {codeVar imports} {
    upvar 1 $codeVar code
    set inLibrary 0
    set sep {}
    foreach i $imports {
	switch -exact -- [lindex $i 0] {
	    import {
		lassign $i key type name args
		append code $sep \n {    } \" $name \"
		set sep ,
	    }
	}
    }
    append code $sep \n {    NULL}
}

# rewriteInitProgram --
#
#	Rewrite the 'stubInit.c' program to contain new definitions
#	of imported routines
#
# Parameters:
#	oldProgram -- Previous content of the 'stubInit.c' file
#	imports    -- Import definitions from 'parseImports'
#
# Results:
#	Returns the new import program

proc rewriteInitProgram {stubDefs oldProgram imports} {
    set newProgram {}
    set sep {}
    set state {}
    foreach piece [split $oldProgram \n] {
	switch -exact -- $state {
	    {} {
		switch -regexp -- $piece {
		    @CREATED@ {
			regsub @CREATED@.* $piece {@CREATED@ } piece
			append piece [clock format [clock seconds] \
					  -format {%Y-%m-%d %H:%M:%SZ} \
					  -gmt 1]
			append piece " by " [file tail $::argv0]
			append piece " from " $stubDefs
		    }
		    @LIBNAMES@ {
			set state ignoring
			accumulateLibNames piece $imports
		    }
		    @SYMNAMES@ {
			set state ignoring
			accumulateSymNames piece $imports
		    }
		}
		append newProgram $sep $piece
		set sep \n

	    }
	    ignoring {
		if {[regexp -- @END@ $piece]} {
		    set state {}
		    append newProgram $sep $piece
		    set sep \n
		}
	    }
	}
    }
    return $newProgram
}

# MAIN PROGRAM - see file header for calling sequence

proc main {stubDefs stubStruct stubInit} {

    # Parse the import definitions

    set imports [parseImports $stubDefs]

    # Write the Stub structure declarations

    set structFile [open $stubStruct w]
    chan configure $structFile -translation lf -encoding utf-8
    writeStructHeader $stubDefs $stubStruct $structFile
    writeStubDeclarations $structFile $imports
    writeStructFooter $stubDefs $structFile
    set stubPrefix [writeStubDefines $structFile $imports]
    chan puts $structFile "MODULE_SCOPE const [file rootname [file tail $stubDefs]]\
                           *${stubPrefix};"
    close $structFile

    # Write the initializations of the function names to import

    set initFile [open $stubInit r+]
    set initProgram [chan read $initFile]
    set initProgram [rewriteInitProgram $stubDefs $initProgram $imports]
    chan seek $initFile 0
    chan truncate $initFile
    chan configure $initFile -translation lf -encoding utf-8
    chan puts -nonewline $initFile $initProgram
    close $initFile

}
main {*}$argv