#! /bin/sh
# -*- tcl -*- \
exec tclsh "$0" ${1+"$@"}

# TMC - Trival Magic Compiler
# === = =====================

# Use cases
# ---------
 
# (-)	Compilation of one or more files in magic(5) syntax into a
#	list of recognizers performing all the checks and mappings
#	encoded in them.
# 
# Command syntax
# --------------
# 
# Ad 1)	tmc namespace magic-file ?magic-file...?
#
#	Compile all magic files list of recognizers, generate a script which
#	assigns the recognizers to $namespace::tests and $namespace::named and
#	write the script to stdout.
# 
# Ad 2)	tmc -merge tclfile namespace magic-file ?magic-file...?
#
#	Same as (1), but does not write to stdout. Instead the part of
#	the 'tclfile' delineated by marker lines containing "BEGIN
#	GENERATED CODE" and "END GENERATED CODE" is replaced with the
#	generated code.

package require Tcl 8.5
set auto_path [linsert $auto_path 0 [file dirname [file normalize [info script]]]] ; # This directory
set auto_path [linsert $auto_path 0 [file dirname [lindex $auto_path end]]]]        ; # and the one above
#puts *\t[join $auto_path \n*\t]
package require fileutil::magic::cfront

# ### ### ### ######### ######### #########
## Internal data and status

namespace eval ::tmc {

    # Path to where the output goes to. An empty string signals that
    # the output is written to stdout. Otherwise it goes to the
    # specified file, which has to exist, and is merged into it.
    #
    # Specified through the optional option '-merge'.

    variable output ""

    # Name of the procedure to generate from the input files.

    variable proc ""

    # List of the input files to process.

    variable magic {}
}

# ### ### ### ######### ######### #########
## External data and status
#
## Only the file merge mode uses external data, which is explicitly
## specified via the command line. It is a template the generated
## recognizer is merged into, completely replacing an existing
## recognizer.

# ### ### ### ######### ######### #########
## Option processing.
## Validate command line.
## Full command line syntax.
##
# tmc ?-merge iofile? procname magic ?magic...?
##

proc ::tmc::processCmdline {} {
    global argv

    variable output
    variable magic
    variable namespace

    set output ""
    set magic  {}
    set namespace ""

    # Process the options, perform basic validation.

    while {[llength $argv]} {
	set opt [lindex $argv 0]
	if {![string match "-*" $opt]} break
	if {$opt eq "-merge"} {
	    if {[llength $argv] < 2} Usage
	    set output [lindex $argv 1]
	    set argv   [lrange $argv 2 end]
	} else {
	    Usage
	}
    }

    # Additional validation, and extraction of the non-option
    # arguments.

    if {[llength $argv] < 2} Usage

    set namespace  [lindex $argv 0]
    set magic [lrange $argv 1 end]

    # Final validation across the whole configuration.

    if {$namespace eq ""} {
	ArgError "Illegal empty namespace name"
    }
    foreach m $magic {
	CheckInput $m {Magic file}
    }
    if {$output ne ""} {
	CheckTheMerge
    }
    return
}

# ### ### ### ######### ######### #########
## Option processing.
## Helpers: Generation of error messages.
## I.  General usage/help message.
## II. Specific messages.
#
# Both write their messages to stderr and then
# exit the application with status 1.
##

proc ::tmc::Usage {} {
    global argv0
    puts stderr "$argv0 wrong#args, expected:\
	    ?-merge iofile? namespace magic magic..."
    exit 1
}

proc ::tmc::ArgError {text} {
    global argv0
    puts stderr "$argv0: $text"
    exit 1
}

proc in {list item} {
    expr {([lsearch -exact $list $item] >= 0)}
}

# ### ### ### ######### ######### #########
## Check existence and permissions of an input/output file or
## directory.

proc ::tmc::CheckInput {f label} {
    if {![file exists $f]} {
	ArgError "Unable to find $label \"$f\""
    } elseif {![file readable $f]} {
	ArgError "$label \"$f\" not readable (permission denied)"
    }
    return
}

proc ::tmc::CheckTheMerge {} {
    variable output

    if {$output eq ""} {
	ArgError "No merge file specified"
    }
    if {![file exists $output]} {
	ArgError "Merge file \"$output\" not found"
    } elseif {![file isfile $output]} {
	ArgError "Merge file \"$output\" is no such (is a directory)"
    } elseif {![file readable $output]} {
	ArgError "Merge file \"$output\" not readable (permission denied)"
    } elseif {![file writable $output]} {
	ArgError "Merge file \"$output\" not writable (permission denied)"
    }
    return
}

# ### ### ### ######### ######### #########
## Helper commands. File reading and writing.

proc ::tmc::Get {f} {
    return [read [set in [open $f r]]][close $in]
}

proc ::tmc::Write {f data} {
    puts -nonewline [set out [open $f w]] $data
    close $out
    return
}

# ### ### ### ######### ######### #########
## Configuation phase, validate command line.

::tmc::processCmdline

# ### ### ### ######### ######### #########
## Helper command implementing the file merge functionality.

proc ::tmc::Merge {f script} {
    set out {}
    set skip 0
    foreach l [split [Get $f] \n] {
	if {$skip == 0} {
	    lappend out $l
	    if {[string match {*BEGIN GENERATED CODE*} $l]} {
		set skip 1
		lappend out $script
	    }
	} elseif {$skip == 1} {
	    if {[string match {*END GENERATED CODE*} $l]} {
		lappend out $l
		set skip 2
	    }
	} else {
	    # Skip == 2
	    lappend out $l
	}
    }
    Write $f [join $out \n]
    return
}

# ### ### ### ######### ######### #########
## Invoking the functionality.

if {[catch {
    # Read and process all input files.
    # Generate commands into a namespace.
    # Write the result either to stdout, or merge
    # into the specified output file.

    set tcl [eval [linsert $tmc::magic 0 \
	    fileutil::magic::cfront::generate \
	    $tmc::namespace]]

    if {$tmc::output eq ""} {
	puts stdout $tcl
    } else {
	::tmc::Merge $tmc::output \n${tcl}\n
    }
} msg]} {
    puts $::errorInfo
    ::tmc::ArgError $msg
}

# ### ### ### ######### ######### #########
exit
