#==============================================================================
#
# mkdepend : generate dependency information from C/C++ files
#
# Copyright (c) 1998, Nat Pryce
#
# Permission is hereby granted, without written agreement and without
# license or royalty fees, to use, copy, modify, and distribute this
# software and its documentation for any purpose, provided that the
# above copyright notice and the following two paragraphs appear in
# all copies of this software.
#
# IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, 
# SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF 
# THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE AUTHOR HAS BEEN ADVISED 
# OF THE POSSIBILITY OF SUCH DAMAGE.
#
# THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT 
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 
# PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" 
# BASIS, AND THE AUTHOR HAS NO OBLIGATION TO PROVIDE  MAINTENANCE, SUPPORT,
# UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
#==============================================================================
#
# Modified heavily by David Gravereaux <davygrvy@pobox.com> about 9/17/2006.
# Original can be found @ 
#	http://web.archive.org/web/20070616205924/http://www.doc.ic.ac.uk/~np2/software/mkdepend.html
#
#==============================================================================
# RCS: @(#) $Id: mkdepend.tcl,v 1.6 2007/12/13 15:28:40 dgp Exp $
#==============================================================================

array set mode_data {}
set mode_data(vc32) {cl -nologo -E}

set source_extensions [list .c .cpp .cxx .cc]

set excludes [list]
if [info exists env(INCLUDE)] {
    set rawExcludes [split [string trim $env(INCLUDE) ";"] ";"]
    foreach exclude $rawExcludes {
	lappend excludes [file normalize $exclude]
    }
}


# openOutput --
#
#	Opens the output file.
#
# Arguments:
#	file	The file to open
#
# Results:
#	None.

proc openOutput {file} {
    global output
    set output [open $file w]
    puts $output "# Automatically generated at [clock format [clock seconds] -format "%Y-%m-%dT%H:%M:%S"] by [info script]\n"
}

# closeOutput --
#
#	Closes output file.
#
# Arguments:
#	none
#
# Results:
#	None.

proc closeOutput {} {
    global output
    if {[string match stdout $output] != 0} {
        close $output
    }
}

# readDepends --
#
#	Read off CCP pipe for #line references.
#
# Arguments:
#	chan	The pipe channel we are reading in.
#
# Results:
#	Raw dependency list pairs.

proc readDepends {chan} {
    set line ""
    array set depends {}

    while {[gets $chan line] != -1} {
        if {[regexp {^#line [0-9]+ \"(.*)\"$} $line dummy fname] != 0} {
	    set fname [file normalize $fname]
            if {![info exists target]} {
		# this is ourself
		set target $fname
		puts stderr "processing [file tail $fname]"
            } else {
		# don't include ourselves as a dependency of ourself.
		if {![string compare $fname $target]} {continue}
		# store in an array so multiple occurances are not counted.
                set depends($target|$fname) ""
            }
        }
    }

    set result {}
    foreach n [array names depends] {
        set pair [split $n "|"]
        lappend result [list [lindex $pair 0] [lindex $pair 1]]
    }

    return $result
}

# writeDepends --
#
#	Write the processed list out to the file.
#
# Arguments:
#	out		The channel to write to.
#	depends		The list of dependency pairs
#
# Results:
#	None.

proc writeDepends {out depends} {
    foreach pair $depends {
        puts $out "[lindex $pair 0] : \\\n\t[join [lindex $pair 1] " \\\n\t"]"
    }
}

# stringStartsWith --
#
#	Compares second string to the beginning of the first.
#
# Arguments:
#	str		The string to test the beginning of.
#	prefix		The string to test against
#
# Results:
#	the result of the comparison.

proc stringStartsWith {str prefix} {
    set front [string range $str 0 [expr {[string length $prefix] - 1}]]
    return [expr {[string compare [string tolower $prefix] \
                                  [string tolower $front]] == 0}]
}

# filterExcludes --
#
#	Remove non-project header files.
#
# Arguments:
#	depends		List of dependency pairs.
#	excludes	List of directories that should be removed
#
# Results:
#	the processed dependency list.

proc filterExcludes {depends excludes} {
    set filtered {}

    foreach pair $depends {
        set excluded 0
        set file [lindex $pair 1]

        foreach dir $excludes {
            if [stringStartsWith $file $dir] {
                set excluded 1
                break;
            }
        }

        if {!$excluded} {
            lappend filtered $pair
        }
    }

    return $filtered
}

# replacePrefix --
#
#	Take the normalized search path and put back the
#	macro name for it.
#
# Arguments:
#	file	filename.
#
# Results:
#	filename properly replaced with macro for it.

proc replacePrefix {file} {
    global srcPathList srcPathReplaceList

    foreach was $srcPathList is $srcPathReplaceList {
	regsub $was $file $is file
    }
    return $file
}

# rebaseFiles --
#
#	Replaces normalized paths with original macro names.
#
# Arguments:
#	depends		Dependency pair list.
#
# Results:
#	The processed dependency pair list.

proc rebaseFiles {depends} {
    set rebased {}
    foreach pair $depends {
        lappend rebased [list \
                [replacePrefix [lindex $pair 0]] \
		[replacePrefix [lindex $pair 1]]]

    }
    return $rebased
}

# compressDeps --
#
#	Compresses same named tragets into one pair with
#	multiple deps.
#
# Arguments:
#	depends	Dependency pair list.
#
# Results:
#	The processed list.

proc compressDeps {depends} {
    array set compressed [list]

    foreach pair $depends {
	lappend compressed([lindex $pair 0]) [lindex $pair 1]
    }

    set result [list]
    foreach n [array names compressed] {
        lappend result [list $n [lsort $compressed($n)]]
    }

    return $result
}

# addSearchPath --
#
#	Adds a new set of path and replacement string to the global list.
#
# Arguments:
#	newPathInfo	comma seperated path and replacement string
#
# Results:
#	None.

proc addSearchPath {newPathInfo} {
    global srcPathList srcPathReplaceList

    set infoList [split $newPathInfo ,]
    lappend srcPathList [file normalize [lindex $infoList 0]]
    lappend srcPathReplaceList [lindex $infoList 1]
}


# displayUsage --
#
#	Displays usage to stderr
#
# Arguments:
#	none.
#
# Results:
#	None.

proc displayUsage {} {
    puts stderr "mkdepend.tcl \[options\] genericDir,macroName compatDir,macroName platformDir,macroName"
}

# readInputListFile --
#
#	Open and read the object file list.
#
# Arguments:
#	objectListFile - name of the file to open.
#
# Results:
#	None.

proc readInputListFile {objectListFile} {
    global srcFileList srcPathList source_extensions
    set f [open $objectListFile r]
    set fl [read $f]
    close $f

    # fix native path seperator so it isn't treated as an escape.
    regsub -all {\\} $fl {/} fl

    # Treat the string as a list so filenames between double quotes are
    # treated as list elements.
    foreach fname $fl {
	# Compiled .res resource files should be ignored.
	if {[file extension $fname] ne ".obj"} {continue}

	# Just filename without path or extension because the path is
	# the build directory, not where the source files are located.
	set baseName [file rootname [file tail $fname]]

	set found 0
	foreach path $srcPathList {
	    foreach ext $source_extensions {
		set test [file join $path ${baseName}${ext}]
		if {[file exist $test]} {
		    lappend srcFileList $test
		    set found 1
		    break
		}
	    }
	    if {$found} break
	}
    }
}

# main --
#
#	The main procedure of this script.
#
# Arguments:
#	none.
#
# Results:
#	None.

proc main {} {
    global argc argv mode mode_data srcFileList srcPathList excludes
    global remove_prefix target_prefix output env

    set srcPathList [list]
    set srcFileList [list]

    if {$argc == 1} {displayUsage}

    # Parse mkdepend input
    for {set i 0} {$i < [llength $argv]} {incr i} {
	switch -glob -- [set arg [lindex $argv $i]] {
	    -vc32 {
		set mode vc32
	    }
	    -bc32 {
		set mode bc32
	    }
	    -wc32 {
		set mode wc32
	    }
	    -lc32 {
		set mode lc32
	    }
	    -mgw32 {
		set mode mgw32
	    }
	    -passthru:* {
		set passthru [string range $arg 10 end]
		regsub -all {"} $passthru {\"} passthru
		regsub -all {\\} $passthru {/} passthru
	    }
	    -out:* {
		openOutput [string range $arg 5 end]
	    }
	    @* {
		set objfile [string range $arg 1 end]
		regsub -all {\\} $objfile {/} objfile
		readInputListFile $objfile
	    }
	    -? - -help - --help {
		displayUsage
		exit 1
	    }
	    default {
		if {![info exist mode]} {
		    puts stderr "mode not set"
		    displayUsage
		}
		addSearchPath $arg
	    }
	}
    }

    # Execute the CPP command and parse output

    foreach srcFile $srcFileList {
	if {[catch {
	    set command "$mode_data($mode) $passthru \"$srcFile\""
	    set input [open |$command r]
	    set depends [readDepends $input]
	    set status [catch {close $input} result]
	    if {$status == 1 && [lindex $::errorCode 0] eq "CHILDSTATUS"} {
		foreach { - pid code } $::errorCode break
		if {$code == 2} {
		    # preprocessor died a cruel death.
		    error $result
		}
	    }
	} err]} {
	    puts stderr "error ocurred: $err\n"
	    continue
	}
	set depends [filterExcludes $depends $excludes]
	set depends [rebaseFiles $depends]
	set depends [compressDeps $depends]
	writeDepends $output $depends
    }

    closeOutput
}

# kick it up.
main
