# module    : base/tex
# copyright : PRAGMA Advanced Document Engineering
# version   : 2005
# author    : Hans Hagen
#
# project   : ConTeXt / eXaMpLe
# concept   : Hans Hagen
# info      : j.hagen@xs4all.nl
# www       : www.pragma-ade.com

# todo:
#
# - write systemcall for mpost to file so that it can be run faster
# - use -8bit and -progname
#

# report ?

require 'base/variables'
require 'base/kpse'
require 'base/system'
require 'base/state'
require 'base/pdf'
require 'base/file'
require 'base/ctx'
require 'base/mp'

class String

    def standard?
        begin
            self == 'standard'
        rescue
            false
        end
    end

end

# class String
    # def utf_bom?
        # self.match(/^\357\273\277/o).length>0 rescue false
    # end
# end

class Array

    def standard?
        begin
            self.include?('standard')
        rescue
            false
        end
    end

    def join_path
        self.join(File::PATH_SEPARATOR)
    end

end

class TEX

    # The make-part of this class was made on a rainy day while listening
    # to "10.000 clowns on a rainy day" by Jan Akkerman. Unfortunately the
    # make method is not as swinging as this live cd.

    include Variables

    @@texengines   = Hash.new
    @@mpsengines   = Hash.new
    @@backends     = Hash.new
    @@mappaths     = Hash.new
    @@runoptions   = Hash.new
    @@draftoptions = Hash.new
    @@texformats   = Hash.new
    @@mpsformats   = Hash.new
    @@prognames    = Hash.new
    @@texmakestr   = Hash.new
    @@texprocstr   = Hash.new
    @@mpsmakestr   = Hash.new
    @@mpsprocstr   = Hash.new

    @@texmethods   = Hash.new
    @@mpsmethods   = Hash.new

    @@pdftex       = 'pdftex' # new default, pdfetex is gone

    @@luafiles     = "luafiles.tmp"
    @@luatarget    = "lua/context"

    # we now drop pdfetex definitely

    # ENV['PATH'].split(File::PATH_SEPARATOR).each do |p|
        # if System.unix? then
            # pp, pe = "#{p}/pdftex"    , "#{p}/pdfetex"
        # else
            # pp, pe = "#{p}/pdftex.exe", "#{p}/pdfetex.exe"
        # end
        # if    FileTest.file?(pe) then # we assume no update
            # @@pdftex = 'pdfetex'
            # break
        # elsif FileTest.file?(pp) then # we assume an update
            # @@pdftex = 'pdftex'
            # break
        # end
    # end

    # ['etex','pdfetex','standard']                .each do |e| @@texengines[e] = @@pdftex    end
    # ['tex','pdftex']                             .each do |e| @@texengines[e] = 'pdftex'    end

    ['tex','etex','pdftex','pdfetex','standard']   .each do |e| @@texengines[e] = 'pdftex'    end
    ['aleph','omega']                              .each do |e| @@texengines[e] = 'aleph'     end
    ['xetex']                                      .each do |e| @@texengines[e] = 'xetex'     end
    ['luatex']                                     .each do |e| @@texengines[e] = 'luatex'    end

    ['metapost','mpost', 'standard']               .each do |e| @@mpsengines[e] = 'mpost'     end

    ['pdfetex','pdftex','pdf','pdftex','standard'] .each do |b| @@backends[b]   = 'pdftex'    end
    ['dvipdfmx','dvipdfm','dpx','dpm']             .each do |b| @@backends[b]   = 'dvipdfmx'  end
    ['xetex','xtx']                                .each do |b| @@backends[b]   = 'xetex'     end
    ['dvips','ps','dvi']                           .each do |b| @@backends[b]   = 'dvips'     end
    ['dvipsone']                                   .each do |b| @@backends[b]   = 'dvipsone'  end
    ['acrobat','adobe','distiller']                .each do |b| @@backends[b]   = 'acrobat'   end
    ['xdv','xdv2pdf']                              .each do |b| @@backends[b]   = 'xdv2pdf'   end

    ['tex','standard']                             .each do |b| @@mappaths[b]   = 'dvips'     end
    ['pdftex','pdfetex']                           .each do |b| @@mappaths[b]   = 'pdftex'    end
    ['aleph','omega','xetex']                      .each do |b| @@mappaths[b]   = 'dvipdfm'   end
    ['dvipdfm', 'dvipdfmx', 'xdvipdfmx']           .each do |b| @@mappaths[b]   = 'dvipdfm'   end
    ['xdv','xdv2pdf']                              .each do |b| @@mappaths[b]   = 'dvips'     end

    # todo norwegian (no)

    ['plain']                                      .each do |f| @@texformats[f] = 'plain'     end
    ['cont-en','en','english','context','standard'].each do |f| @@texformats[f] = 'cont-en'   end
    ['cont-nl','nl','dutch']                       .each do |f| @@texformats[f] = 'cont-nl'   end
    ['cont-de','de','german']                      .each do |f| @@texformats[f] = 'cont-de'   end
    ['cont-it','it','italian']                     .each do |f| @@texformats[f] = 'cont-it'   end
    ['cont-fr','fr','french']                      .each do |f| @@texformats[f] = 'cont-fr'   end
    ['cont-cz','cz','czech']                       .each do |f| @@texformats[f] = 'cont-cz'   end
    ['cont-ro','ro','romanian']                    .each do |f| @@texformats[f] = 'cont-ro'   end
    ['cont-uk','uk','british']                     .each do |f| @@texformats[f] = 'cont-uk'   end
    ['mptopdf']                                    .each do |f| @@texformats[f] = 'mptopdf'   end

    ['latex']                                      .each do |f| @@texformats[f] = 'latex.ltx' end

    ['plain','mpost']                              .each do |f| @@mpsformats[f] = 'plain'     end
    ['metafun','context','standard']               .each do |f| @@mpsformats[f] = 'metafun'   end

    # no 'standard' progname ! / beware, when using texexec we always use the context/metafun values

    ['pdftex','pdfetex','aleph','omega',
     'xetex','luatex']                             .each do |p| @@prognames[p]  = 'context'   end
    ['mpost']                                      .each do |p| @@prognames[p]  = 'metafun'   end

    ['plain','default','standard','mptopdf']       .each do |f| @@texmethods[f] = 'plain'     end
    ['cont-en','cont-nl','cont-de','cont-it',
     'cont-fr','cont-cz','cont-ro','cont-uk']      .each do |f| @@texmethods[f] = 'context'   end
    ['latex']                                      .each do |f| @@texmethods[f] = 'latex'     end

    ['plain','default','standard']                 .each do |f| @@mpsmethods[f] = 'plain'     end
    ['metafun']                                    .each do |f| @@mpsmethods[f] = 'metafun'   end

    @@texmakestr['plain'] = "\\dump"
    @@mpsmakestr['plain'] = "\\dump"

    ['cont-en','cont-nl','cont-de','cont-it',
     'cont-fr','cont-cz','cont-ro','cont-uk']      .each do |f| @@texprocstr[f] = "\\emergencyend"  end

    # @@runoptions['xetex']   = ['--output-driver \\\"-d 4 -V 5\\\"'] # we need the pos pass
    # @@runoptions['xetex']   = ['--8bit','-no-pdf'] # from now on we assume (x)dvipdfmx to be used
    @@runoptions['xetex']   = ['--8bit','-output-driver="xdvipdfmx -E -d 4 -V 5"']
    @@runoptions['pdfetex'] = ['--8bit']           # obsolete
    @@runoptions['pdftex']  = ['--8bit']           # pdftex is now pdfetex
    @@runoptions['luatex']  = ['--file-line-error']
    @@runoptions['aleph']   = ['--8bit']
    @@runoptions['mpost']   = ['--8bit']

    @@draftoptions['pdftex'] = ['--draftmode']

    @@booleanvars = [
        'batchmode', 'nonstopmode', 'fast', 'fastdisabled', 'silentmode', 'final',
        'paranoid', 'notparanoid', 'nobanner', 'once', 'allpatterns', 'draft',
        'nompmode', 'nomprun', 'automprun', 'combine',
        'nomapfiles', 'local',
        'arrange', 'noarrange',
        'forcexml', 'foxet',
        'alpha', 'beta', 'luatex',
        'mpyforce', 'forcempy',
        'forcetexutil', 'texutil',
        'globalfile', 'autopath',
        'purge', 'purgeall', 'keep', 'autopdf', 'xpdf', 'simplerun', 'verbose',
        'nooptionfile', 'nobackend', 'noctx', 'utfbom',
        'mkii',
    ]
    @@stringvars = [
        'modefile', 'result', 'suffix', 'response', 'path',
        'filters', 'usemodules', 'environments', 'separation', 'setuppath',
        'arguments', 'input', 'output', 'randomseed', 'modes', 'mode', 'filename',
        'ctxfile', 'printformat', 'paperformat', 'paperoffset',
        'timeout', 'passon'
    ]
    @@standardvars = [
        'mainlanguage', 'bodyfont', 'language'
    ]
    @@knownvars = [
        'engine', 'distribution', 'texformats', 'mpsformats', 'progname', 'interface',
        'runs', 'backend'
    ]

    @@extrabooleanvars = []
    @@extrastringvars = []

    def booleanvars
        [@@booleanvars,@@extrabooleanvars].flatten.uniq
    end
    def stringvars
        [@@stringvars,@@extrastringvars].flatten.uniq
    end
    def standardvars
        [@@standardvars].flatten.uniq
    end
    def knownvars
        [@@knownvars].flatten.uniq
    end
    def allbooleanvars
        [@@booleanvars,@@extrabooleanvars].flatten.uniq
    end
    def allstringvars
        [@@stringvars,@@extrastringvars,@@standardvars,@@knownvars].flatten.uniq
    end

    def setextrastringvars(vars)
        @@extrastringvars << vars
    end
    def setextrabooleanvars(vars)
        @@extrabooleanvars << vars
    end

    # def jobvariables(names=nil)
        # if [names ||[]].flatten.size == 0 then
            # names = [allbooleanvars,allstringvars].flatten
        # end
        # data = Hash.new
        # names.each do |name|
            # if allbooleanvars.include?(name) then
                # data[name] = if getvariable(name) then "yes" else "no" end
            # else
                # data[name] = getvariable(name)
            # end
        # end
        # data
    # end

    # def setjobvariables(names=nil)
        # assignments = Array.new
        # jobvariables(names).each do |k,v|
            # assignments << "#{k}=\{#{v}\}"
        # end
        # "\setvariables[exe][#{assignments.join(", ")}]"
    # end

    @@temprunfile = 'texexec'
    @@temptexfile = 'texexec.tex'

    def initialize(logger=nil)
        if @logger = logger then
            def report(str='')
                @logger.report(str)
            end
        else
            def report(str='')
                puts(str)
            end
        end
        @cleanups    = Array.new
        @variables   = Hash.new
        @startuptime = Time.now
        # options
        booleanvars.each do |k|
            setvariable(k,false)
        end
        stringvars.each do |k|
            setvariable(k,'')
        end
        standardvars.each do |k|
            setvariable(k,'standard')
        end
        setvariable('distribution', Kpse.distribution)
        setvariable('texformats',   defaulttexformats)
        setvariable('mpsformats',   defaultmpsformats)
        setvariable('progname',     'standard') # or ''
        setvariable('interface',    'standard')
        setvariable('engine',       'standard') # replaced by tex/mpsengine
        setvariable('backend',      'pdftex')
        setvariable('runs',         '8')
        setvariable('randomseed',   rand(1440).to_s) # we want the same seed for one run
        # files
        setvariable('files',        [])
        # defaults
        setvariable('texengine',    'standard')
        setvariable('mpsengine',    'standard')
        setvariable('backend',      'standard')
        setvariable('error',        '')
    end

    def error?
        not getvariable('error').empty?
    end

    def runtime
        Time.now - @startuptime
    end

    def reportruntime
        report("runtime: #{runtime}")
    end

    def runcommand(something)
        command = [something].flatten.join(' ')
        report("running: #{command}") if getvariable('verbose')
        system(command)
    end

    def inspect(name=nil)
        if ! name || name.empty? then
            name = [booleanvars,stringvars,standardvars,knownvars]
        end
        str = '' # allocate
        [name].flatten.each do |n|
            if str = getvariable(n) then
                str = str.join(" ") if str.class == Array
                unless (str.class == String) && str.empty? then
                    report("option '#{n}' is set to '#{str}'")
                end
            end
        end
    end

    def tempfilename(suffix='')
        @@temprunfile + if suffix.empty? then '' else ".#{suffix}" end
    end

    def cleanup
        @cleanups.each do |name|
            begin
                File.delete(name) if FileTest.file?(name)
            rescue
                report("unable to delete #{name}")
            end
        end
    end

    def cleanuptemprunfiles
        begin
            Dir.glob("#{@@temprunfile}*").each do |name|
                if File.file?(name) && (File.splitname(name)[1] !~ /(pdf|dvi)/o) then
                    File.delete(name) rescue false
                end
            end
        rescue
        end
        ['mpgraph.mp'].each do |file|
            (File.delete(file) if (FileTest.size?(file) rescue 10) < 10) rescue false
        end
    end

    def backends() @@backends.keys.sort end

    def texengines() @@texengines.keys.sort end
    def mpsengines() @@mpsengines.keys.sort end
    def texformats() @@texformats.keys.sort end
    def mpsformats() @@mpsformats.keys.sort end

    def defaulttexformats() ['en','nl','mptopdf'] end
    def defaultmpsformats() ['metafun']           end

    def texmakeextras(format) @@texmakestr[format] || '' end
    def mpsmakeextras(format) @@mpsmakestr[format] || '' end
    def texprocextras(format) @@texprocstr[format] || '' end
    def mpsprocextras(format) @@mpsprocstr[format] || '' end

    def texmethod(format) @@texmethods[str] || @@texmethods['standard'] end
    def mpsmethod(format) @@mpsmethods[str] || @@mpsmethods['standard'] end

    def runoptions(engine)
        options = if getvariable('draft') then @@draftoptions[engine] else [] end
        begin
            if str = getvariable('passon') then
                options = [options,str.split(' ')].flatten
            end
        rescue
        end
        if @@runoptions.key?(engine) then
            [options,@@runoptions[engine]].flatten.join(' ')
        else
            options.join(' ')
        end
    end

    # private

    def cleanuplater(name)
        begin
            @cleanups.push(File.expand_path(name))
        rescue
            @cleanups.push(name)
        end
    end

    def openedfile(name)
        begin
            f = File.open(name,'w')
        rescue
            report("file '#{File.expand_path(name)}' cannot be opened for writing")
            return nil
        else
            cleanuplater(name) if f
            return f
        end
    end

    def prefixed(format,engine)
        # format
        case engine
           when /etex|pdftex|pdfetex|aleph|xetex|luatex/io then
               "*#{format}"
           else
               format
        end
    end

    def quoted(str)
        if str =~ /^[^\"].* / then "\"#{str}\"" else str end
    end

    def getarrayvariable(str='')
        str = getvariable(str)
        if str.class == String then str.split(',') else str.flatten end
    end

    def validtexformat(str) validsomething(str,@@texformats,'tex')    end
    def validmpsformat(str) validsomething(str,@@mpsformats,'mp' )    end
    def validtexengine(str) validsomething(str,@@texengines,'pdftex') end
    def validmpsengine(str) validsomething(str,@@mpsengines,'mpost' ) end

    def validtexmethod(str) [validsomething(str,@@texmethods)].flatten.first end
    def validmpsmethod(str) [validsomething(str,@@mpsmethods)].flatten.first end

    def validsomething(str,something,type=nil)
        if str then
            list = [str].flatten.collect do |s|
                if something[s] then
                    something[s]
                elsif type && s =~ /\.#{type}$/ then
                    s
                else
                    nil
                end
            end .compact.uniq
            if list.length>0 then
                if str.class == String then list.first else list end
            else
                false
            end
        else
            false
        end
    end

    def validbackend(str)
        if str && @@backends.key?(str) then
            @@backends[str]
        else
            @@backends['standard']
        end
    end

    def validprogname(str)
        if str then
            [str].flatten.each do |s|
                return @@prognames[s] if @@prognames.key?(s)
            end
        else
            return nil
        end
    end

    # we no longer support the & syntax

    def formatflag(engine=nil,format=nil)
        case getvariable('distribution')
            when 'standard' then prefix = "--fmt"
            when /web2c/io  then prefix = web2cformatflag(engine)
            when /miktex/io then prefix = "--undump"
                            else return ""
        end
        if format then
            # if engine then
                # "#{prefix}=#{engine}/#{format}"
            # else
                "#{prefix}=#{format}"
            # end
        else
            prefix
        end
    end

    def web2cformatflag(engine=nil)
        # funny that we've standardized on the fmt suffix (at the cost of
        # upward compatibility problems) but stuck to the bas/mem/fmt flags
        if engine then
            case validmpsengine(engine)
                when /mpost/ then "-mem"
                when /mfont/ then "-bas"
                else              "-fmt"
            end
        else
            "-fmt"
        end
    end

    def prognameflag(progname=nil)
        case getvariable('distribution')
            when 'standard' then prefix = "-progname"
            when /web2c/io  then prefix = "-progname"
            when /miktex/io then prefix = "-alias"
                            else return ""
        end
        if progname and not progname.empty? then
            "#{prefix}=#{progname}"
        else
            prefix
        end
    end

    def iniflag() # should go to kpse and kpse should become texenv
        if Kpse.miktex? then
            "-initialize"
        else
            "--ini"
        end
    end
    def tcxflag(file="natural.tcx")
        if Kpse.miktex? then
            "-tcx=#{file}"
        else
            "-translate-file=#{file}"
        end
    end

    def filestate(file)
        File.mtime(file).strftime("%d/%m/%Y %H:%M:%S")
    end

    # will go to context/process context/listing etc

    def contextversion # ook elders gebruiken
        filename = Kpse.found('context.tex')
        version = 'unknown'
        begin
            if FileTest.file?(filename) && IO.read(filename).match(/\\contextversion\{(\d+\.\d+\.\d+.*?)\}/) then
                version = $1
            end
        rescue
        end
        return version
    end

    def cleanupluafiles
        File.delete(@@luafiles) rescue false
    end

    def compileluafiles
        begin
            Dir.glob("lua/context/*.luc").each do |luc|
                File.delete(luc) rescue false
            end
        rescue
        end
        if data = (IO.readlines(@@luafiles) rescue nil) then
            report("compiling lua files (using #{File.expand_path(@@luafiles)})")
            begin
                Dir.makedirs(@@luatarget) rescue false
                data.each do |line|
                    luafile = line.chomp
                    lucfile = File.basename(luafile).gsub(/\..*?$/,'') + ".luc"
                    if runcommand(["luac","-s","-o",quoted(File.join(Dir.getwd,@@luatarget,lucfile)),quoted(luafile)]) then
                        report("#{File.basename(luafile)} converted to #{File.basename(lucfile)}")
                    else
                        report("#{File.basename(luafile)} not converted to #{File.basename(lucfile)}")
                    end
                end
            rescue
                report("fatal error in compilation")
            end
        else
            report("no lua compilations needed")
        end
        File.delete(@@luafiles) rescue false
    end

    # we need engine methods

    def makeformats

        checktestversion

        report("using search method '#{Kpse.searchmethod}'")
        if getvariable('fast') then
            report('using existing database')
        else
            report('updating file database')
            Kpse.update
            if getvariable('luatex') then
                begin
                    luatools = `texmfstart luatools --format=texmfscripts luatools.lua`.chomp.strip
                    unless luatools.empty? then
                        runcommand(["luatex","--luaonly #{luatools}","--generate","--verbose"])
                    end
                rescue
                    report("run 'luatex --luaonly pathto/luatools.lua --generate' manually")
                    exit
                end
            end
        end
        # goody
        if getvariable('texformats') == 'standard' then
            setvariable('texformats',[getvariable('interface')]) unless getvariable('interface').empty?
        end
        # prepare
        texformats = validtexformat(getarrayvariable('texformats'))
        mpsformats = validmpsformat(getarrayvariable('mpsformats'))
        texengine  = validtexengine(getvariable('texengine'))
        mpsengine  = validmpsengine(getvariable('mpsengine'))
        # save current path
        savedpath = Dir.getwd
        # generate tex formats
        unless texformats || mpsformats then
            report('provide valid format (name.tex, name.mp, ...) or format id (metafun, en, nl, ...)')
            setvariable('error','no format specified')
        end
        if texformats && texengine then
            report("using tex engine #{texengine}")
            texformatpath = if getvariable('local') then '.' else Kpse.formatpath(texengine,true) end
            # can be empty, to do
            report("using tex format path #{texformatpath}")
            Dir.chdir(texformatpath) rescue false
            if FileTest.writable?(texformatpath) then
                if texformats.length > 0 then
                    makeuserfile
                    makeresponsefile
                end
                if texengine == 'luatex' then
                    cleanupluafiles
                    texformats.each do |texformat|
                        report("generating tex format #{texformat}")
                        flags = ['--ini','--compile']
                        flags << '--verbose' if getvariable('verbose')
                        flags << '--mkii'    if getvariable('mkii')
                        run_luatools("#{flags.join(" ")} #{texformat}")
                    end
                    compileluafiles
                else
                    texformats.each do |texformat|
                        report("generating tex format #{texformat}")
                        progname = validprogname([getvariable('progname'),texformat,texengine])
                        runcommand([quoted(texengine),prognameflag(progname),iniflag,tcxflag,prefixed(texformat,texengine),texmakeextras(texformat)])
                    end
                end
            else
                report("unable to make format due to lack of permissions")
                texformatpath = ''
            end
        else
            texformatpath = ''
        end
        # generate mps formats
        if mpsformats && mpsengine then
            report("using mp engine #{mpsengine}")
            mpsformatpath = if getvariable('local') then '.' else Kpse.formatpath(mpsengine,false) end
            report("using mps format path #{mpsformatpath}")
            Dir.chdir(mpsformatpath) rescue false
            if FileTest.writable?(mpsformatpath) then
                mpsformats.each do |mpsformat|
                    report("generating mps format #{mpsformat}")
                    progname = validprogname([getvariable('progname'),mpsformat,mpsengine])
                    if not runcommand([quoted(mpsengine),prognameflag(progname),iniflag,tcxflag,runoptions(mpsengine),mpsformat,mpsmakeextras(mpsformat)]) then
                        setvariable('error','no format made')
                    end
                end
            else
                report("unable to make format due to lack of permissions")
                mpsformatpath = ''
                setvariable('error','file permission problem')
            end
        else
            mpsformatpath = ''
        end
        # check for problems
        report("")
        report("tex engine path: #{texformatpath}") unless texformatpath.empty?
        report("mps engine path: #{mpsformatpath}") unless mpsformatpath.empty?
        report("")
        [['fmt','tex'],['mem','mps']].each do |f|
            [[texformatpath,'global'],[mpsformatpath,'global'],[savedpath,'current']].each do |p|
                begin
                    Dir.chdir(p[0])
                rescue
                else
                    Dir.glob("*.#{f[0]}").each do |file|
                        report("#{f[1]}: #{filestate(file)} > #{File.expand_path(file)} (#{File.size(file)})")
                    end
                end
            end
        end
        begin
            lucdir = File.join(texformatpath,@@luatarget)
            Dir.chdir(lucdir)
        rescue
        else
            Dir.glob("*.luc").each do |file|
                report("luc: #{filestate(file)} > #{File.expand_path(file)} (#{File.size(file)})")
            end
        end
        # to be sure, go back to current path
        begin
            Dir.chdir(savedpath)
        rescue
        end
        # finalize
        cleanup
        report("")
        reportruntime
    end

    def checkcontext

        # todo : report texmf.cnf en problems

        # basics
        report("current distribution: #{Kpse.distribution}")
        report("context source date: #{contextversion}")
        formatpaths = Kpse.formatpaths
        globpattern = "**/{#{formatpaths.join(',')}}/*/*.{fmt,efmt,ofmt,xfmt,mem}"
        report("format path: #{formatpaths.join(' ')}")
        # utilities
        report('start of analysis')
        results = Array.new
        ['texexec','texutil','ctxtools'].each do |program|
            result = `texmfstart #{program} --help`
            result.sub!(/.*?(#{program}[^\n]+)\n.*/mi) do $1 end
            results.push("#{result}")
        end
        # formats
        cleanuptemprunfiles
        if formats = Dir.glob(globpattern) then
            formats.sort.each do |name|
                cleanuptemprunfiles
                if f = open(tempfilename('tex'),'w') then
                    # kind of aleph-run-out-of-par safe
                    f << "\\starttext\n"
                    f << "  \\relax test \\relax\n"
                    f << "\\stoptext\n"
                    f << "\\endinput\n"
                    f.close
                    if FileTest.file?(tempfilename('tex')) then
                        format = File.basename(name)
                        engine = if name =~ /(pdftex|pdfetex|aleph|xetex|luatex)[\/\\]#{format}/ then $1 else '' end
                        if engine.empty? then
                            engineflag = ""
                        else
                            engineflag = "--engine=#{$1}"
                        end
                        case format
                            when /cont\-([a-z]+)/ then
                                interface = $1.sub(/cont\-/,'')
                                results.push('')
                                results.push("testing interface #{interface}")
                                flags = ['--noctx','--process','--batch','--once',"--interface=#{interface}",engineflag]
                                # result = Kpse.pipescript('texexec',tempfilename,flags)
                                result = runtexexec([tempfilename], flags, 1)
                                if FileTest.file?("#{@@temprunfile}.log") then
                                    logdata = IO.read("#{@@temprunfile}.log")
                                    if logdata =~ /^\s*This is (.*?)[\s\,]+(.*?)$/mois then
                                        if validtexengine($1.downcase) then
                                            results.push("#{$1} #{$2.gsub(/\(format.*$/,'')}".strip)
                                        end
                                    end
                                    if logdata =~ /^\s*(ConTeXt)\s+(.*int:\s+[a-z]+.*?)\s*$/mois then
                                        results.push("#{$1} #{$2}".gsub(/\s+/,' ').strip)
                                    end
                                else
                                    results.push("format #{format} does not work")
                                end
                            when /metafun/ then
                                # todo
                            when /mptopdf/ then
                                # todo
                        end
                    else
                        results.push("error in creating #{tempfilename('tex')}")
                    end
                end
                cleanuptemprunfiles
            end
        end
        report('end of analysis')
        report
        results.each do |line|
            report(line)
        end
        cleanuptemprunfiles

    end

    private

    def makeuserfile
        language = getvariable('language')
        mainlanguage = getvariable('mainlanguage')
        bodyfont = getvariable('bodyfont')
        if f = openedfile("cont-fmt.tex") then
            f << "\\unprotect"
            case language
                when 'all' then
                    f << "\\preloadallpatterns\n"
                when '' then
                    f << "% no language presets\n"
                when 'standard'
                    f << "% using defaults\n"
                else
                    languages = language.split(',')
                    languages.each do |l|
                        f << "\\installlanguage[\\s!#{l}][\\c!state=\\v!start]\n"
                    end
                    mainlanguage = languages.first
            end
            unless mainlanguage == 'standard' then
                f << "\\setupcurrentlanguage[\\s!#{mainlanguage}]\n";
            end
            unless bodyfont == 'standard' then
                # ~ will become obsolete when lmr is used
                f << "\\definetypescriptsynonym[cmr][#{bodyfont}]"
                # ~ is already obsolete for some years now
                f << "\\definefilesynonym[font-cmr][font-#{bodyfont}]\n"
            end
            f << "\\protect\n"
            f << "\\endinput\n"
            f.close
        end
    end

    def makeresponsefile
        interface = getvariable('interface')
        if f = openedfile("mult-def.tex") then
            case interface
                when 'standard' then
                    f << "% using default response interface"
                else
                    f << "\\def\\currentresponses\{#{interface}\}\n"
            end
            f << "\\endinput\n"
            f.close
        end
    end

    private  # will become baee/context

    @@preamblekeys = [
        ['tex','texengine'],
        ['engine','texengine'],
        ['program','texengine'],
        ['translate','tcxfilter'],
        ['tcx','tcxfilter'],
        ['output','backend'],
        ['mode','mode'],
        ['ctx','ctxfile'],
        ['version','contextversion'],
        ['format','texformats'],
        ['interface','texformats'],
    ]

    @@re_utf_bom = /^\357\273\277/o

    def scantexpreamble(filename)
        begin
            if FileTest.file?(filename) and tex = File.open(filename) then
                bomdone = false
                while str = tex.gets and str.chomp! do
                    unless bomdone then
                        if str.sub!(@@re_utf_bom, '')
                            report("utf mode forced (bom found)")
                            setvariable('utfbom',true)
                        end
                        bomdone = true
                    end
                    if str =~ /^\%\s*(.*)/o then
                        # we only accept lines with key=value pairs
                        vars, ok = Hash.new, true
                        $1.split(/\s+/o).each do |s|
                            k, v = s.split('=')
                            if k && v then
                                vars[k] = v
                            else
                                ok = false
                                break
                            end
                        end
                        if ok then
                            # we have a valid line
                            @@preamblekeys.each do |v|
                                setvariable(v[1],vars[v[0]]) if vars.key?(v[0]) && vars[v[0]]
                            end
                            break
                        end
                    else
                        break
                    end
                end
                tex.close
            end
        rescue
            # well, let's not worry too much
        end
    end

    def scantexcontent(filename)
        if FileTest.file?(filename) and tex = File.open(filename) then
            while str = tex.gets do
                case str.chomp
                    when /^\%/o then
                        # next
                    when /\\(starttekst|stoptekst|startonderdeel|startdocument|startoverzicht)/o then
                        setvariable('texformats','nl') ; break
                    when /\\(stelle|verwende|umgebung|benutze)/o then
                        setvariable('texformats','de') ; break
                    when /\\(stel|gebruik|omgeving)/o then
                        setvariable('texformats','nl') ; break
                    when /\\(use|setup|environment)/o then
                        setvariable('texformats','en') ; break
                    when /\\(usa|imposta|ambiente)/o then
                        setvariable('texformats','it') ; break
                    when /(height|width|style)=/o then
                        setvariable('texformats','en') ; break
                    when /(hoehe|breite|schrift)=/o then
                        setvariable('texformats','de') ; break
                    when /(hoogte|breedte|letter)=/o then
                        setvariable('texformats','nl') ; break
                    when /(altezza|ampiezza|stile)=/o then
                        setvariable('texformats','it') ; break
                    when /externfiguur/o then
                        setvariable('texformats','nl') ; break
                    when /externalfigure/o then
                        setvariable('texformats','en') ; break
                    when /externeabbildung/o then
                        setvariable('texformats','de') ; break
                    when /figuraesterna/o then
                        setvariable('texformats','it') ; break
                end
            end
            tex.close
        end

    end

    private # will become base/context

    def pushresult(filename,resultname)
        fname = File.unsuffixed(filename)
        rname = File.unsuffixed(resultname)
        if ! rname.empty? && (rname != fname) then
            report("outputfile #{rname}")
            ['tuo','tuc','log','dvi','pdf'].each do |s|
                File.silentrename(File.suffixed(fname,s),File.suffixed('texexec',s))
            end
            ['tuo','tuc'].each do |s|
                File.silentrename(File.suffixed(rname,s),File.suffixed(fname,s)) if FileTest.file?(File.suffixed(rname,s))
            end
        end
    end

    def popresult(filename,resultname)
        fname = File.unsuffixed(filename)
        rname = File.unsuffixed(resultname)
        if ! rname.empty? && (rname != fname) then
            report("renaming #{fname} to #{rname}")
            ['tuo','tuc','log','dvi','pdf'].each do |s|
                File.silentrename(File.suffixed(fname,s),File.suffixed(rname,s))
            end
            report("restoring #{fname}")
            unless $fname == 'texexec' then
                ['tuo','tuc','log','dvi','pdf'].each do |s|
                    File.silentrename(File.suffixed('texexec',s),File.suffixed(fname,s))
                end
            end
        end
    end

    def makestubfile(rawname,rawbase,forcexml=false)
        if tmp = openedfile(File.suffixed(rawbase,'run')) then
            tmp << "\\starttext\n"
            if forcexml then
                # tmp << checkxmlfile(rawname)
                tmp << "\\processXMLfilegrouped{#{rawname}}\n"
            else
                tmp << "\\processfile{#{rawname}}\n"
            end
            tmp << "\\stoptext\n"
            tmp.close
            return "run"
        else
            return File.splitname(rawname)[1]
        end
    end

    # def checkxmlfile(rawname)
        # tmp = ''
        # if FileTest.file?(rawname) && (xml = File.open(rawname)) then
            # xml.each do |line|
                # case line
                    # when /<\?context\-directive\s+(\S+)\s+(\S+)\s+(\S+)\s*(.*?)\s*\?>/o then
                        # category, key, value, rest = $1, $2, $3, $4
                        # case category
                            # when 'job' then
                                # case key
                                    # when 'control' then
                                        # setvariable(value,if rest.empty? then true else rest end)
                                    # when 'mode', 'modes' then
                                        # tmp << "\\enablemode[#{value}]\n"
                                    # when 'stylefile', 'environment' then
                                        # tmp << "\\environment #{value}\n"
                                    # when 'module' then
                                        # tmp << "\\usemodule[#{value}]\n"
                                    # when 'interface' then
                                        # contextinterface = value
                                    # when 'ctxfile' then
                                        # setvariable('ctxfile', value)
                                        # report("using source driven ctxfile #{value}")
                                # end
                        # end
                    # when /<[a-z]+/io then # beware of order, first pi test
                        # break
                # end
            # end
            # xml.close
        # end
        # return tmp
    # end

    def extendvariable(name,value)
        set = getvariable(name).split(',')
        set << value
        str = set.uniq.join(',')
        setvariable(name,str)
    end

    def checkxmlfile(rawname)
        if FileTest.file?(rawname) && (xml = File.open(rawname)) then
            xml.each do |line|
                case line
                    when /<\?context\-directive\s+(\S+)\s+(\S+)\s+(\S+)\s*(.*?)\s*\?>/o then
                        category, key, value, rest = $1, $2, $3, $4
                        case category
                            when 'job' then
                                case key
                                    when 'control' then
                                        setvariable(value,if rest.empty? then true else rest end)
                                    when /^(mode)(s|)$/ then
                                        extendvariable('modes',value)
                                    when /^(stylefile|environment)(s|)$/ then
                                        extendvariable('environments',value)
                                    when /^(use|)(module)(s|)$/ then
                                        extendvariable('usemodules',value)
                                    when /^(filter)(s|)$/ then
                                        extendvariable('filters',value)
                                    when 'interface' then
                                        contextinterface = value
                                    when 'ctxfile' then
                                        setvariable('ctxfile', value)
                                        report("using source driven ctxfile #{value}")
                                end
                        end
                    when /<[a-z]+/io then # beware of order, first pi test
                        break
                end
            end
            xml.close
        end
    end

end

class TEX

    def timedrun(delay, &block)
        delay = delay.to_i rescue 0
        if delay > 0 then
            begin
                report("job started with timeout '#{delay}'")
                timeout(delay) do
                    yield block
                end
            rescue TimeoutError
                report("job aborted due to timeout '#{delay}'")
            rescue
                report("job aborted due to error")
            else
                report("job finished within timeout '#{delay}'")
            end
        else
            yield block
        end
    end

    def processtex # much to do: mp, xml, runs etc
        setvariable('texformats',[getvariable('interface')]) unless getvariable('interface').empty?
        getarrayvariable('files').each do |filename|
            setvariable('filename',filename)
            report("processing document '#{filename}'")
            timedrun(getvariable('timeout')) do
                processfile
            end
        end
        reportruntime
    end

    def processmptex
        getarrayvariable('files').each do |filename|
            setvariable('filename',filename)
            report("processing graphic '#{filename}'")
            runtexmp(filename)
        end
        reportruntime
    end

    private

    def load_map_files(filename) # tui basename
        # c \usedmapfile{=}{lm-texnansi}
        begin
            str = ""
            IO.read(filename).scan(/^c\s+\\usedmapfile\{(.*?)\}\{(.*?)\}\s*$/o) do
                str << "\\loadmapfile[#{$2}.map]\n"
            end
        rescue
            return ""
        else
            return str
        end
    end

    public

    def run_luatools(args)
        # dirty trick: we know that the lua path is relative to the ruby path; of course this
        # will not work well when stubs are used
        [(ENV["_CTX_K_S_texexec_"] or ENV["_CTX_K_S_THREAD_"] or ENV["TEXMFSTART.THREAD"]), File.dirname($0)].each do |path|
            if path then
                script = "#{path}/../lua/luatools.lua"
                if FileTest.file?(script) then
                    return runcommand("luatex --luaonly #{script} #{args}")
                end
            end
        end
        return runcommand("texmfstart luatools #{args}")
    end

    def processmpgraphic
        getarrayvariable('files').each do |filename|
            setvariable('filename',filename)
            report("processing graphic '#{filename}'")
            runtexmp(filename,'',false) # no purge
            mapspecs = load_map_files(File.suffixed(filename,'temp','tui'))
            unless getvariable('keep') then
                # not enough: purge_mpx_files(filename)
                Dir.glob(File.suffixed(filename,'temp*','*')).each do |fname|
                    File.delete(fname) unless File.basename(filename) == File.basename(fname)
                end
            end
            begin
                data = IO.read(File.suffixed(filename,'log'))
                basename = filename.sub(/\.mp$/, '')
                if data =~ /output files* written\:\s*(.*)$/mois then
                    files, number, range, list = $1.split(/\s+/), 0, false, []
                    files.each do |fname|
                        if fname =~ /^.*\.(\d+)$/ then
                            if range then
                                (number+1 .. $1.to_i).each do |i|
                                    list << i
                                end
                                range = false
                            else
                                number = $1.to_i
                                list << number
                            end
                        elsif fname =~ /\.\./ then
                            range = true
                        else
                            range = false
                            next
                        end
                    end
                    begin
                        if getvariable('combine') then
                            fullname = "#{basename}.#{number}"
                            File.open("texexec.tex",'w') do |f|
                                f << "\\setupoutput[pdftex]\n"
                                f << "\\setupcolors[state=start]\n"
                                f << mapspecs
                                f << "\\starttext\n"
                                list.each do |number|
                                    f << "\\startTEXpage\n"
                                    f << "\\convertMPtoPDF{#{fullname}}{1}{1}"
                                    f << "\\stopTEXpage\n"
                                end
                                f << "\\stoptext\n"
                            end
                            report("converting graphic '#{fullname}'")
                            runtex("texexec.tex")
                            pdffile = File.suffixed(basename,'pdf')
                            File.silentrename("texexec.pdf",pdffile)
                            report ("#{basename}.* converted to #{pdffile}")
                        else
                            list.each do |number|
                                begin
                                    fullname = "#{basename}.#{number}"
                                    File.open("texexec.tex",'w') do |f|
                                        f << "\\setupoutput[pdftex]\n"
                                        f << "\\setupcolors[state=start]\n"
                                        f << mapspecs
                                        f << "\\starttext\n"
                                        f << "\\startTEXpage\n"
                                        f << "\\convertMPtoPDF{#{fullname}}{1}{1}"
                                        f << "\\stopTEXpage\n"
                                        f << "\\stoptext\n"
                                    end
                                    report("converting graphic '#{fullname}'")
                                    runtex("texexec.tex")
                                    if files.length>1 then
                                        pdffile = File.suffixed(basename,number.to_s,'pdf')
                                    else
                                        pdffile = File.suffixed(basename,'pdf')
                                    end
                                    File.silentrename("texexec.pdf",pdffile)
                                    report ("#{fullname} converted to #{pdffile}")
                                end
                            end
                        end
                    rescue
                        report ("error when converting #{fullname} (#{$!})")
                    end
                end
            rescue
                report("error in converting #{filename}")
            end
        end
        reportruntime
    end

    def processmpstatic
        if filename = getvariable('filename') then
            filename += ".mp" unless filename =~ /\..+?$/
            if FileTest.file?(filename) then
                begin
                    data = IO.read(filename)
                    File.open("texexec.tex",'w') do |f|
                        f << "\\setupoutput[pdftex]\n"
                        f << "\\setupcolors[state=start]\n"
                        data.sub!(/^%mpenvironment\:\s*(.*?)$/mois) do
                            f << $1
                            "\n"
                        end
                        f << "\\starttext\n"
                        f << "\\startMPpage\n"
                        f << data.gsub(/end\.*\s*$/m, '') # a bit of a hack
                        f << "\\stopMPpage\n"
                        f << "\\stoptext\n"
                    end
                    report("converting static '#{filename}'")
                    runtex("texexec.tex")
                    pdffile = File.suffixed(filename,'pdf')
                    File.silentrename("texexec.pdf",pdffile)
                    report ("#{filename} converted to #{pdffile}")
                rescue
                    report("error in converting #{filename} (#{$!}")
                end
            end
        end
        reportruntime
    end

    def processmpxtex
        getarrayvariable('files').each do |filename|
            setvariable('filename',filename)
            report("processing text of graphic '#{filename}'")
            processmpx(filename,false,true,true)
        end
        reportruntime
    end

    def deleteoptionfile(rawname)
        ['top','top.keep'].each do |suffix|
            begin
                File.delete(File.suffixed(rawname,suffix))
            rescue
            end
        end
    end

    def makeoptionfile(rawname, jobname, jobsuffix, finalrun, fastdisabled, kindofrun, currentrun=1)
        begin
            # jobsuffix = orisuffix
            if topname = File.suffixed(rawname,'top') and opt = File.open(topname,'w') then
                report("writing option file #{topname}")
                # local handies
                opt << "\% #{topname}\n"
                opt << "\\unprotect\n"
                if getvariable('utfbom') then
                    opt << "\\enableregime[utf]"
                end
                opt << "\\setupsystem[\\c!n=#{kindofrun},\\c!m=#{currentrun}]\n"
                progname = validprogname(['metafun']) # [getvariable('progname'),mpsformat,mpsengine]
                opt << "\\def\\MPOSTformatswitch\{#{prognameflag(progname)} #{formatflag('mpost')}=\}\n"
                if getvariable('batchmode') then
                    opt << "\\batchmode\n"
                end
                if getvariable('nonstopmode') then
                    opt << "\\nonstopmode\n"
                end
                if getvariable('paranoid') then
                    opt << "\\def\\maxreadlevel{1}\n"
                end
                if (str = File.unixfied(getvariable('modefile'))) && ! str.empty? then
                    opt << "\\readlocfile{#{str}}{}{}\n"
                end
                if (str = File.unixfied(getvariable('result'))) && ! str.empty? then
                    opt << "\\setupsystem[file=#{str}]\n"
                elsif (str = getvariable('suffix')) && ! str.empty? then
                    opt << "\\setupsystem[file=#{jobname}.#{str}]\n"
                end
                opt << "\\setupsystem[\\c!method=2]\n" # 1=oldtexexec 2=newtexexec (obsolete)
                opt << "\\setupsystem[\\c!type=#{Tool.ruby_platform()}]\n"
                if (str = File.unixfied(getvariable('path'))) && ! str.empty? then
                    opt << "\\usepath[#{str}]\n" unless str.empty?
                end
                if (str = getvariable('mainlanguage').downcase) && ! str.empty? && ! str.standard? then
                    opt << "\\setuplanguage[#{str}]\n"
                end
                if str = validbackend(getvariable('backend')) then
                    opt << "\\setupoutput[#{str}]\n"
                elsif str = validbackend(getvariable('output')) then
                    opt << "\\setupoutput[#{str}]\n"
                end
                if getvariable('color') then
                    opt << "\\setupcolors[\\c!state=\\v!start]\n"
                end
                if getvariable('nompmode') || getvariable('nomprun') || getvariable('automprun') then
                    opt << "\\runMPgraphicsfalse\n"
                end
                if getvariable('fast') && ! getvariable('fastdisabled') then
                    opt << "\\fastmode\n"
                end
                if getvariable('silentmode') then
                    opt << "\\silentmode\n"
                end
                if (str = getvariable('separation')) && ! str.empty? then
                    opt << "\\setupcolors[\\c!split=#{str}]\n"
                end
                if (str = getvariable('setuppath')) && ! str.empty? then
                    opt << "\\setupsystem[\\c!directory=\{#{str}\}]\n"
                end
                if (str = getvariable('paperformat')) && ! str.empty? && ! str.standard? then
                    if str =~ /^([a-z]+\d+)([a-z]+\d+)$/io then # A5A4 A4A3 A2A1 ...
                        opt << "\\setuppapersize[#{$1.upcase}][#{$2.upcase}]\n"
                    else # ...*...
                        pf = str.upcase.split(/[x\*]/o)
                        pf << pf[0] if pf.size == 1
                        opt << "\\setuppapersize[#{pf[0]}][#{pf[1]}]\n"
                    end
                end
                if (str = getvariable('background')) && ! str.empty? then
                    opt << "\\defineoverlay[whatever][{\\externalfigure[#{str}][\\c!factor=\\v!max]}]\n"
                    opt << "\\setupbackgrounds[\\v!page][\\c!background=whatever]\n"
                end
                if getvariable('centerpage') then
                    opt << "\\setuplayout[\\c!location=\\v!middle,\\c!marking=\\v!on]\n"
                end
                if getvariable('nomapfiles') then
                    opt << "\\disablemapfiles\n"
                end
                if getvariable('noarrange') then
                    opt << "\\setuparranging[\\v!disable]\n"
                elsif getvariable('arrange') then
                    arrangement = Array.new
                    if finalrun then
                        arrangement << "\\v!doublesided" unless getvariable('noduplex')
                        case getvariable('printformat')
                            when ''         then arrangement << "\\v!normal"
                            when /.*up/oi   then arrangement << ["2UP","\\v!rotated"]
                            when /.*down/oi then arrangement << ["2DOWN","\\v!rotated"]
                            when /.*side/oi then arrangement << ["2SIDE","\\v!rotated"]
                        end
                    else
                        arrangement << "\\v!disable"
                    end
                    opt << "\\setuparranging[#{arrangement.flatten.join(',')}]\n" if arrangement.size > 0
                end
                # we handle both "--mode" and "--modes", else "--mode" is
                # mapped onto "--modefile"
                if (str = getvariable('modes')) && ! str.empty? then
                    opt << "\\enablemode[#{str}]\n"
                end
                if (str = getvariable('mode')) && ! str.empty? then
                    opt << "\\enablemode[#{str}]\n"
                end
                if (str = getvariable('arguments')) && ! str.empty? then
                    opt << "\\setupenv[#{str}]\n"
                end
                if (str = getvariable('randomseed')) && ! str.empty? then
                    report("using randomseed #{str}")
                    opt << "\\setupsystem[\\c!random=#{str}]\n"
                end
                if (str = getvariable('input')) && ! str.empty? then
                    opt << "\\setupsystem[inputfile=#{str}]\n"
                else
                    opt << "\\setupsystem[inputfile=#{rawname}]\n"
                end
                if (str = getvariable('pages')) && ! str.empty? then
                    if str.downcase == 'odd' then
                        opt << "\\chardef\\whichpagetoshipout=1\n"
                    elsif str.downcase == 'even' then
                        opt << "\\chardef\\whichpagetoshipout=2\n"
                    else
                        pagelist = Array.new
                        str.split(/\,/).each do |page|
                            pagerange = page.split(/\D+/o)
                            if pagerange.size > 1 then
                                pagerange.first.to_i.upto(pagerange.last.to_i) do |p|
                                    pagelist << p.to_s
                                end
                            else
                                pagelist << page
                            end
                        end
                        opt << "\\def\\pagestoshipout\{#{pagelist.join(',')}\}\n";
                    end
                end
                opt << "\\protect\n";
                begin getvariable('filters'     ).split(',').uniq.each do |f| opt << "\\useXMLfilter[#{f}]\n"   end ; rescue ; end
                begin getvariable('usemodules'  ).split(',').uniq.each do |m| opt << "\\usemodule[#{m}]\n"      end ; rescue ; end
                begin getvariable('environments').split(',').uniq.each do |e| opt << "\\environment #{e}\n"     end ; rescue ; end
              # this will become:
              # begin getvariable('environments').split(',').uniq.each do |e| opt << "\\useenvironment[#{e}]\n" end ; rescue ; end
                opt << "\\endinput\n"
                opt.close
           else
                report("unable to write option file #{topname}")
            end
        rescue
            report("fatal error in writing option file #{topname} (#{$!})")
        end
    end

    def takeprecautions
        ENV['MPXCOMAND'] = '0' # else loop
        if getvariable('paranoid') then
            ENV['SHELL_ESCAPE'] = ENV['SHELL_ESCAPE'] || 'f'
            ENV['OPENOUT_ANY']  = ENV['OPENOUT_ANY']  || 'p'
            ENV['OPENIN_ANY']   = ENV['OPENIN_ANY']   || 'p'
        elsif getvariable('notparanoid') then
            ENV['SHELL_ESCAPE'] = ENV['SHELL_ESCAPE'] || 't'
            ENV['OPENOUT_ANY']  = ENV['OPENOUT_ANY']  || 'a'
            ENV['OPENIN_ANY']   = ENV['OPENIN_ANY']   || 'a'
        end
        if ENV['OPENIN_ANY'] && (ENV['OPENIN_ANY'] == 'p') then # first test redundant
            setvariable('paranoid', true)
        end
        if ENV.key?('SHELL_ESCAPE') && (ENV['SHELL_ESCAPE'] == 'f') then
            setvariable('automprun',true)
        end
        done = false
        ['TXRESOURCES','MPRESOURCES','MFRESOURCES'].each do |res|
            [getvariable('runpath'),getvariable('path')].each do |pat|
                unless pat.empty? then
                    if ENV.key?(res) then
                        ENV[res] = if ENV[res].empty? then pat else pat + ":" + ENV[res] end
                    else
                        ENV[res] = pat
                    end
                    report("setting #{res} to #{ENV[res]}") unless done
                end
            end
            done = true
        end
    end

    def checktestversion
        #
        # one can set TEXMFALPHA and TEXMFBETA for test versions
        # but keep in mind that the format as well as the test files
        # then need the --alpha or --beta flag
        #
        done, tree = false, ''
        ['alpha', 'beta'].each do |what|
            if getvariable(what) then
                if ENV["TEXMF#{what.upcase}"] then
                    done, tree = true, ENV["TEXMF#{what.upcase}"]
                elsif ENV["TEXMFLOCAL"] then
                    done, tree = true, File.join(File.dirname(ENV['TEXMFLOCAL']), "texmf-#{what}")
                end
            end
            break if done
        end
        if done then
            tree = tree.strip
            ENV['TEXMFPROJECT'] = tree
            report("using test tree '#{tree}'")
            ['MP', 'MF', 'TX'].each do |ctx|
                ENV['CTXDEV#{ctx}PATH'] = ''
            end
            unless (FileTest.file?(File.join(tree,'ls-r')) || FileTest.file?(File.join(tree,'ls-R'))) then
                report("no ls-r/ls-R file for tree '#{tree}' (run: mktexlsr #{tree})")
            end
        end
        # puts `kpsewhich --expand-path=$TEXMF`
        # exit
    end

    def runtex(filename)
        checktestversion
        texengine = validtexengine(getvariable('texengine'))
        texformat = validtexformat(getarrayvariable('texformats').first)
        report("tex engine: #{texengine}")
        report("tex format: #{texformat}")
        if texengine && texformat then
            fixbackendvars(@@mappaths[texengine])
            if texengine == "luatex" then
                # currently we use luatools to start luatex but some day we should
                # find a clever way to directly call luatex (problem is that we need
                # to feed the explicit location of the format and lua initialization
                # file)
                run_luatools("--fmt=#{texformat} #{filename}")
            else
                progname = validprogname([getvariable('progname'),texformat,texengine])
                runcommand([quoted(texengine),prognameflag(progname),formatflag(texengine,texformat),tcxflag,runoptions(texengine),filename,texprocextras(texformat)])
            end
            # true
        else
            false
        end
    end

    def runmp(mpname,mpx=false)
        checktestversion
        mpsengine = validmpsengine(getvariable('mpsengine'))
        mpsformat = validmpsformat(getarrayvariable('mpsformats').first)
        if mpsengine && mpsformat then
            ENV["MPXCOMMAND"] = "0" unless mpx
            progname = validprogname([getvariable('progname'),mpsformat,mpsengine])
            runcommand([quoted(mpsengine),prognameflag(progname),formatflag(mpsengine,mpsformat),tcxflag,runoptions(mpsengine),mpname,mpsprocextras(mpsformat)])
            # runcommand([quoted(mpsengine),formatflag(mpsengine,mpsformat),tcxflag,runoptions(mpsengine),mpname,mpsprocextras(mpsformat)])
            true
        else
            false
        end
    end

    def runtexmp(filename,filetype='',purge=true)
        checktestversion
        mpname = File.suffixed(filename,filetype,'mp')
        if File.atleast?(mpname,25) then
            # first run needed
            File.silentdelete(File.suffixed(mpname,'mpt'))
            doruntexmp(mpname,nil,true,purge)
            mpgraphics = checkmpgraphics(mpname)
            mplabels = checkmplabels(mpname)
            if mpgraphics || mplabels then
                # second run needed
                doruntexmp(mpname,mplabels,true,purge)
            else
                # no labels
            end
        end
    end

    def runtexmpjob(filename,filetype='')
        checktestversion
        mpname = File.suffixed(filename,filetype,'mp')
        if File.atleast?(mpname,25) && (data = File.silentread(mpname)) then
            textranslation = if data =~ /^\%\s+translate.*?\=([\w\d\-]+)/io then $1 else '' end
            mpjobname = if data =~ /collected graphics of job \"(.+?)\"/io then $1 else '' end
            if ! mpjobname.empty? and File.unsuffixed(filename) =~ /#{mpjobname}/ then # don't optimize
                options = Array.new
                options.push("--mptex")
                options.push("--nomp")
                options.push("--mpyforce") if getvariable('forcempy') || getvariable('mpyforce')
                options.push("--translate=#{textranslation}") unless textranslation.empty?
                options.push("--batch") if getvariable('batchmode')
                options.push("--nonstop") if getvariable('nonstopmode')
                options.push("--output=ps") # options.push("--dvi")
                options.push("--nobackend")
                return runtexexec(mpname,options,2)
            end
        end
        return false
    end

    def runtexutil(filename=[], options=['--ref','--ij','--high'], old=false)
        filename.each do |fname|
            if old then
                Kpse.runscript('texutil',fname,options)
            else
                begin
                    logger = Logger.new('TeXUtil')
                    if tu = TeXUtil::Converter.new(logger) and tu.loaded(fname) then
                        ok = tu.processed && tu.saved && tu.finalized
                    end
                rescue
                    Kpse.runscript('texutil',fname,options)
                end
            end
        end
    end

    def runluacheck(jobname)
        if false then
            # test-pos.tex / 6 meg tua file: 18.6 runtime
            old, new = File.suffixed(jobname,'tua'), File.suffixed(jobname,'tuc')
            if FileTest.file?(old) then
                report("converting #{old} into #{new}")
                system("luac -s -o #{new} #{old}")
            end
        else
            # test-pos.tex / 6 meg tua file: 17.5 runtime
            old, new = File.suffixed(jobname,'tua'), File.suffixed(jobname,'tuc')
            if FileTest.file?(old) then
                report("renaming #{old} into #{new}")
                File.rename(old,new) rescue false
            end
        end
    end

    # 1=tex 2=mptex 3=mpxtex 4=mpgraphic 5=mpstatic

    def runtexexec(filename=[], options=[], mode=nil)
        begin
            if mode and job = TEX.new(@logger) then
                options.each do |option|
                    case option
                        when /^\-*(.*?)\=(.*)$/o then
                            job.setvariable($1,$2)
                        when /^\-*(.*?)$/o then
                            job.setvariable($1,true)
                    end
                end
                job.setvariable("files",filename)
                case mode
                    when 1 then job.processtex
                    when 2 then job.processmptex
                    when 3 then job.processmpxtex
                    when 4 then job.processmpgraphic
                    when 5 then job.processmpstatic
                end
                job.inspect && Kpse.inspect if getvariable('verbose')
                return true
            else
                Kpse.runscript('texexec',filename,options)
            end
        rescue
            Kpse.runscript('texexec',filename,options)
        end
    end

    def fixbackendvars(backend)
        if backend then
            report("fixing backend map path for #{backend}") if getvariable('verbose')
            ENV['backend']     = backend ;
            ENV['progname']    = backend unless validtexengine(backend)
            ENV['TEXFONTMAPS'] = ['.',"\$TEXMF/fonts/map/{#{backend},pdftex,dvips,}//",'./fonts//'].join_path
        else
            report("unable to fix backend map path") if getvariable('verbose')
        end
    end

    def runbackend(rawname)
        unless getvariable('nobackend') then
            case validbackend(getvariable('backend'))
                when 'dvipdfmx' then
                    fixbackendvars('dvipdfm')
                    runcommand("dvipdfmx -d 4 -V 5 #{File.unsuffixed(rawname)}")
                when 'xetex'    then
                    # xetex now runs its own backend
                    xdvfile = File.suffixed(rawname,'xdv')
                    if FileTest.file?(xdvfile) then
                        fixbackendvars('dvipdfm')
                        runcommand("xdvipdfmx -q -d 4 -V 5 -E #{xdvfile}")
                    end
                when 'xdv2pdf' then
                    xdvfile = File.suffixed(rawname,'xdv')
                    if FileTest.file?(xdvfile) then
                        fixbackendvars('xdv2pdf')
                        runcommand("xdv2pdf #{xdvfile}")
                    end
                when 'dvips' then
                    fixbackendvars('dvips')
                    mapfiles = ''
                    begin
                        if tuifile = File.suffixed(rawname,'tui') and FileTest.file?(tuifile) then
                            IO.read(tuifile).scan(/^c \\usedmapfile\{.\}\{(.*?)\}\s*$/o) do
                                mapfiles += "-u +#{$1} " ;
                            end
                        end
                    rescue
                        mapfiles = ''
                    end
                    runcommand("dvips #{mapfiles} #{File.unsuffixed(rawname)}")
                when 'pdftex'   then
                    # no need for postprocessing
                else
                    report("no postprocessing needed")
            end
        end
    end

    def processfile

        takeprecautions
        report("using search method '#{Kpse.searchmethod}'") if getvariable('verbose')
        rawname    = getvariable('filename')
        jobname    = getvariable('filename')
        suffix     = getvariable('suffix')
        result     = getvariable('result')
        forcexml   = getvariable('forcexml')
        runonce    = getvariable('once')
        finalrun   = getvariable('final') || (getvariable('arrange') && ! getvariable('noarrange'))
        globalfile = getvariable('globalfile')

        if getvariable('autopath') then
            jobname = File.basename(jobname)
            inppath = File.dirname(jobname)
        else
            inppath = ''
        end

        jobname, jobsuffix = File.splitname(jobname,'tex')

        jobname = File.unixfied(jobname)
        inppath = File.unixfied(inppath)
        result  = File.unixfied(result)

        orisuffix = jobsuffix # still needed ?

        setvariable('nomprun',true) if orisuffix == 'mpx' # else cylic run

        PDFview.setmethod('xpdf') if getvariable('xpdf')

        PDFview.closeall if getvariable('autopdf')

        if jobsuffix =~ /^(htm|html|xhtml|xml|fo|fox|rlg|exa)$/io then
            forcexml = true
        end

        dummyfile = false

        # fuzzy code snippet: (we kunnen kpse: prefix gebruiken)

        unless FileTest.file?(File.suffixed(jobname,jobsuffix)) then
            if FileTest.file?(rawname + '.tex') then
                jobname = rawname.dup
                jobsuffix  = 'tex'
            end
        end

        # we can have funny names, like 2005.10.10 (given without suffix)

        rawname = jobname + '.' + jobsuffix

        rawpath = File.dirname(rawname)
        rawbase = File.basename(rawname)

        unless FileTest.file?(rawname) then
            inppath.split(',').each do |ip|
                break if dummyfile = FileTest.file?(File.join(ip,rawname))
            end
        end

        if dummyfile || forcexml then
            jobsuffix = makestubfile(rawname,rawbase,forcexml)
            checkxmlfile(rawname)
        end


        # preprocess files

        unless getvariable('noctx') then
            ctx = CtxRunner.new(rawname,@logger)
            if getvariable('ctxfile').empty? then
                if rawname == rawbase then
                    ctx.manipulate(File.suffixed(rawname,'ctx'),'jobname.ctx')
                else
                    ctx.manipulate(File.suffixed(rawname,'ctx'),File.join(rawpath,'jobname.ctx'))
                end
            else
                ctx.manipulate(File.suffixed(getvariable('ctxfile'),'ctx'))
            end
            ctx.savelog(File.suffixed(rawbase,'ctl'))

            envs  = ctx.environments
            mods  = ctx.modules
            flags = ctx.flags

            flags.each do |f|
                f.sub!(/^\-+/,'')
                if f =~ /^(.*?)=(.*)$/ then
                    setvariable($1,$2)
                else
                    setvariable(f,true)
                end
            end

            report("using flags #{flags.join(' ')}") if flags.size > 0

            # merge environment and module specs

            envs << getvariable('environments') unless getvariable('environments').empty?
            mods << getvariable('modules')      unless getvariable('modules')     .empty?

            envs = envs.uniq.join(',')
            mods = mods.uniq.join(',')

            report("using search method '#{Kpse.searchmethod}'") if getvariable('verbose')
            report("using environments #{envs}") if envs.length > 0
            report("using modules #{mods}")      if mods.length > 0

            setvariable('environments', envs)
            setvariable('modules',      mods)
        end

        # end of preprocessing and merging

        if globalfile || FileTest.file?(rawname) then

            if not dummyfile and not globalfile then
                scantexpreamble(rawname)
                scantexcontent(rawname) if getvariable('texformats').standard?
            end
            result = File.suffixed(rawname,suffix) unless suffix.empty?

            pushresult(rawbase,result)

            method = validtexmethod(validtexformat(getvariable('texformats')))

            report("tex processing method: #{method}")

            case method

                when 'context' then
                    if getvariable('simplerun') || runonce then
                        makeoptionfile(rawbase,jobname,orisuffix,true,true,3,1) unless getvariable('nooptionfile')
                        ok = runtex(if dummyfile || forcexml then rawbase else rawname end)
                        if ok then
                            ok = runtexutil(rawbase) if getvariable('texutil') || getvariable('forcetexutil')
                            runluacheck(rawbase)
                            runbackend(rawbase)
                            popresult(rawbase,result)
                        end
                        if getvariable('keep') then
                            ['top','log','run'].each do |suffix|
                                File.silentrename(File.suffixed(rawbase,suffix),File.suffixed(rawbase,suffix+'.keep'))
                            end
                        end
                    else
# goto tmp/jobname when present
                        mprundone, ok, stoprunning = false, true, false
                        texruns, nofruns = 0, getvariable('runs').to_i
                        state = FileState.new
                        ['tub','tuo','tuc'].each do |s|
                            state.register(File.suffixed(rawbase,s))
                        end
                        if getvariable('automprun') then # check this
                            ['mprun','mpgraph'].each do |s|
                                state.register(File.suffixed(rawbase,s,'mp'),'randomseed')
                            end
                        end
                        while ! stoprunning && (texruns < nofruns) && ok do
                            texruns += 1
                            report("TeX run #{texruns}")
                            unless getvariable('nooptionfile') then
                                if texruns == nofruns then
                                    makeoptionfile(rawbase,jobname,orisuffix,false,false,4,texruns) # last
                                elsif texruns == 1 then
                                    makeoptionfile(rawbase,jobname,orisuffix,false,false,1,texruns) # first
                                else
                                    makeoptionfile(rawbase,jobname,orisuffix,false,false,2,texruns) # unknown
                                end
                            end
# goto .
                            ok = runtex(File.suffixed(if dummyfile || forcexml then rawbase else rawname end,jobsuffix))
# goto tmp/jobname when present
                            if ok && (nofruns > 1) then
                                unless getvariable('nompmode') then
                                    mprundone = runtexmpjob(rawbase, "mpgraph")
                                    mprundone = runtexmpjob(rawbase, "mprun")
                                end
                                ok = runtexutil(rawbase)
                                runluacheck(rawbase)
                                state.update
                                stoprunning = state.stable?
                            end
                        end
                        if not ok then
                            setvariable('error','error in tex file')
                        end
                        if (nofruns == 1) && getvariable('texutil') then
                            ok = runtexutil(rawbase)
                            runluacheck(rawbase)
                        end
                        if ok && finalrun && (nofruns > 1) then
                            makeoptionfile(rawbase,jobname,orisuffix,true,finalrun,4,texruns) unless getvariable('nooptionfile')
                            report("final TeX run #{texruns}")
# goto .
                            ok = runtex(File.suffixed(if dummyfile || forcexml then rawbase else rawname end,jobsuffix))
# goto tmp/jobname when present
                        end
                        if getvariable('keep') then
                            ['top','log','run'].each do |suffix|
                                File.silentrename(File.suffixed(rawbase,suffix),File.suffixed(rawbase,suffix+'.keep'))
                            end
                        else
                            File.silentrename(File.suffixed(rawbase,'top'),File.suffixed(rawbase,'tmp'))
                        end
                        # ['tmp','top','log'].each do |s| # previous tuo file / runtime option file / log file
                             # File.silentdelete(File.suffixed(rawbase,s))
                        # end
                        if ok then
# goto .
                            runbackend(rawbase)
                            popresult(rawbase,result)
# goto tmp/jobname when present
# skip next
                        end
                        if true then # autopurge
                            begin
                                File.open(File.suffixed(rawbase, 'tuo')) do |f|
                                    ok = 0
                                    f.each do |line|
                                        case ok
                                            when 1 then
                                                # next line is empty
                                                ok = 2
                                            when 2 then
                                                if line =~ /^\%\s+\>\s+(.*?)\s+(\d+)/mois then
                                                    filename, n = $1, $2
                                                    done = File.delete(filename) rescue false
                                                    if done && getvariable('verbose') then
                                                        report("deleting #{filename} (#{n} times used)")
                                                    end
                                                else
                                                    break
                                                end
                                            else
                                                if line =~ /^\%\s+temporary files\:\s+(\d+)/mois then
                                                    if $1.to_i == 0 then
                                                        break
                                                    else
                                                        ok = 1
                                                    end
                                                end
                                        end
                                    end
                                end
                            rescue
                                # report("fatal error #{$!}")
                            end
                        end
                    end

                    Kpse.runscript('ctxtools',rawbase,'--purge')       if getvariable('purge')
                    Kpse.runscript('ctxtools',rawbase,'--purge --all') if getvariable('purgeall')
# till here
                when 'latex' then

                    ok = runtex(rawname)

                else

                    ok = runtex(rawname)

            end

            if (dummyfile or forcexml) and FileTest.file?(rawbase) then
                begin
                    File.delete(File.suffixed(rawbase,'run'))
                rescue
                    report("unable to delete stub file")
                end
            end

            if ok and getvariable('autopdf') then
                PDFview.open(File.suffixed(if result.empty? then rawbase else result end,'pdf'))
            end

        else
            report("nothing to process")
        end

    end

    # The labels are collected in the mergebe hash. Here we merge the relevant labels
    # into beginfig/endfig. We could as well do this in metafun itself. Maybe some
    # day ... (it may cost a bit of string space but that is cheap nowadays).

    def doruntexmp(mpname,mergebe=nil,context=true,purge=true)
        texfound = false
        mpname = File.suffixed(mpname,'mp')
        mpcopy = File.suffixed(mpname,'mp.copy')
        mpkeep = File.suffixed(mpname,'mp.keep')
        setvariable('mp.file',mpname)
        setvariable('mp.line','')
        setvariable('mp.error','')
        if mpdata = File.silentread(mpname) then
            mpdata.gsub!(/^\#.*\n/o,'')
            File.silentrename(mpname,mpcopy)
            texfound = mergebe || (mpdata =~ /btex .*? etex/mo)
            if mp = openedfile(mpname) then
                if mergebe then
                    mpdata.gsub!(/beginfig\s*\((\d+)\)\s*\;(.+?)endfig\s*\;/mo) do
                        n, str = $1, $2
                        if str =~ /^(.*?)(verbatimtex.*?etex)\s*\;(.*)$/mo then
                            "beginfig(#{n})\;\n#{$1}#{$2}\;\n#{mergebe[n]}\n#{$3}\;endfig\;\n"
                        else
                            "beginfig(#{n})\;\n#{mergebe[n]}\n#{str}\;endfig\;\n"
                        end
                    end
                    unless mpdata =~ /beginfig\s*\(\s*0\s*\)/o then
                        mp << mergebe['0'] if mergebe.key?('0')
                    end
                end
                mp << MPTools::splitmplines(mpdata)
                mp << "\n"
                mp << "end"
                mp << "\n"
                mp.close
            end
            processmpx(mpname,true,true,purge) if texfound
            if getvariable('batchmode') then
                options = ' --interaction=batch'
            elsif getvariable('nonstopmode') then
                options = ' --interaction=nonstop'
            else
                options = ''
            end
            # todo plain|mpost|metafun
            ok = runmp(mpname)
            if f = File.silentopen(File.suffixed(mpname,'log')) then
                while str = f.gets do
                    if str =~ /^l\.(\d+)\s(.*?)\n/o then
                        setvariable('mp.line',$1)
                        setvariable('mp.error',$2)
                        break
                    end
                end
                f.close
            end
            File.silentrename(mpname, mpkeep)
            File.silentrename(mpcopy, mpname)
        end
    end

    # todo: use internal mptotext function and/or turn all btex/etex into textexts

    def processmpx(mpname,force=false,context=true,purge=true)
        unless force then
            mpname = File.suffixed(mpname,'mp')
            if File.atleast?(mpname,10) && (data = File.silentread(mpname)) then
                if data =~ /(btex|etex|verbatimtex|textext)/o then
                    force = true
                end
            end
        end
        if force then
            begin
                mptex = File.suffixed(mpname,'temp','tex')
                mpdvi = File.suffixed(mpname,'temp','dvi')
                mplog = File.suffixed(mpname,'temp','log')
                mpmpx = File.suffixed(mpname,'mpx')
                File.silentdelete(mptex)
                if true then
                    report("using internal mptotex converter")
                    ok = MPTools::mptotex(mpname,mptex,'context')
                else
                    command = "mpto #{mpname} > #{mptex}"
                    report(command) if getvariable('verbose')
                    ok = system(command)
                end
                # not "ok && ..." because of potential problem with return code and redirect (>)
                if FileTest.file?(mptex) && File.appended(mptex, "\\end\n") then
                    # to be replaced by runtexexec([filenames],options,1)
                    if localjob = TEX.new(@logger) then
                        localjob.setvariable('files',mptex)
                        localjob.setvariable('backend','dvips')
                        localjob.setvariable('engine',getvariable('engine')) unless getvariable('engine').empty?
                        localjob.setvariable('once',true)
                        localjob.setvariable('nobackend',true)
                        if context then
                            localjob.setvariable('texformats',[getvariable('interface')]) unless getvariable('interface').empty?
                        elsif getvariable('interface').empty? then
                            localjob.setvariable('texformats',['plain'])
                        else
                            localjob.setvariable('texformats',[getvariable('interface')])
                        end
                        localjob.processtex
                        ok = true # todo
                    else
                        ok = false
                    end
                    # so far
                    command = "dvitomp #{mpdvi} #{mpmpx}"
                    report(command) if getvariable('verbose')
                    ok = ok && FileTest.file?(mpdvi) && system(command)
                    purge_mpx_files(mpname) if purge
                end
            rescue
                # error in processing mpx file
            end
        end
    end

    def purge_mpx_files(mpname)
        unless getvariable('keep') then
            ['tex', 'log', 'tui', 'tuo', 'tuc', 'top'].each do |suffix|
                File.silentdelete(File.suffixed(mpname,'temp',suffix))
            end
        end
    end

    def checkmpgraphics(mpname)
        mpoptions = ''
        if getvariable('makempy') then
            mpoptions += " --makempy "
        end
        if getvariable('mpyforce') || getvariable('forcempy') then
            mpoptions += " --force "
        else
            mponame = File.suffixed(mpname,'mpo')
            mpyname = File.suffixed(mpname,'mpy')
            return false unless File.atleast?(mponame,32)
            mpochecksum = FileState.new.checksum(mponame)
            return false if mpochecksum.empty?
            # where does the checksum get into the file?
            # maybe let texexec do it?
            # solution: add one if not present or update when different
            if f = File.silentopen(mpyname) then
                str = f.gets.chomp
                f.close
                if str =~ /^\%\s*mpochecksum\s*\:\s*(\d+)/o then
                    return false if mpochecksum == $1
                end
            end
        end
        return Kpse.runscript('makempy',mpname)
    end

    def checkmplabels(mpname)
        mpname = File.suffixed(mpname,'mpt')
        if File.atleast?(mpname,10) && (mp = File.silentopen(mpname)) then
            labels = Hash.new
            while str = mp.gets do
                t = if str =~ /^%\s*setup\s*:\s*(.*)$/o then $1 else '' end
                if str =~ /^%\s*figure\s*(\d+)\s*:\s*(.*)$/o then
                    labels[$1] = labels[$1] || ''
                    unless t.empty? then
                        labels[$1] += "#{t}\n"
                        t = ''
                    end
                    labels[$1] += "#{$2}\n"
                end
            end
            mp.close
            if labels.size>0 then
                return labels
            else
                return nil
            end
        end
        return nil
    end

end
