#!/usr/bin/env texlua  

VERSION = "0.23b"

--[[
     musixtex.lua: processes MusiXTeX files using xml2pmx and/or prepmx and/or pmxab 
     and/or autosp as pre-processors (and deletes intermediate files)

     (c) Copyright 2011-2021 Bob Tennent rdt@cs.queensu.ca
                             and Dirk Laurie dirk.laurie@gmail.com

     This program is free software; you can redistribute it and/or modify it
     under the terms of the GNU General Public License as published by the
     Free Software Foundation; either version 2 of the License, or (at your
     option) any later version.

     This program is distributed in the hope that it will be useful,
     but WITHOUT ANY WARRANTY; without even the implied warranty of
     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
     Public License for more details.

     You should have received a copy of the GNU General Public License along
     with this program; if not, write to the Free Software Foundation, Inc.,
     51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

--]]

--[[

  ChangeLog:

     version 0.23b 2020-05-27  RDT
       improve the -h output

     version 0.23a 2020-08-14  RDT
       pmxprep files now deleted by xml2pmx

     version 0.23  2020-05-21 RDT
       added support for xml2pmx pre-preprocessing

     version 0.22  2020-03-20 RDT
       add -X option
       add -version, --version, -help, --help options

     version 0.21  2018-07-27  RDT
       add -P option.

     version 0.20  2018-06-11  RDT
       remove .mx1 file before tex processing

     version 0.19   2017-12-10 RDT
       Allow non-standard extensions.
       Add -M and -A options.

     version 0.18   2017-06-13 RDT
       Allow autosp to generate .ltx files

     version 0.17a   2017-01-08 RDT
       Added -D option.
       Avoid writing or concatenating a nil value.

     version 0.16e  2016-03-02 DL
       missing version information (caused by batchmode in 0.16c) fixed

     version 0.16d  2016-03-02 RDT
       filename argument in autosp failure message fixed

     version 0.16c  2016-02-24 RDT
       -interaction batchmode for -q
       report_error reports only the first error

     version 0.16b  2016-02-20 DL
       Improved help message as suggested by Bob Tennent.

     version 0.16a  2015-12-30 DL
       Corrects bug in -g option reported by Christian Mondrup.

     version 0.16  2015-12-24 DL
       Versions read from tempfile and written to musixtex.log.

     version 0.15  2015-12-03 DL
       Option -q added, which redirects most screen output into 
       a temporary file. If an error occurs at the TeX stage, processing 
       halts immediately and the tail of the log file is sent to stderr.

     version 0.14  2015-12-01 DL
       Writes `musixtex.log` and deletes other logfiles (except with -i)
       Hierarchy of extensions emphasized in usage() and consistently
         used elsewhere.
       Unknown option-like arguments reported and ignored, not treated
         as filenames
       Warnings and error messages generated by musixtex.lua start with
         respctively `!` and `!!`.

     version 0.13  2015-11-30 RDT 
      Process .mtx and .pmx files (suggested by Dirk Laurie)
      exit_code is now an error count
      Use pmxab exit code to distinguish between errors and warnings

     version 0.12 2015-11-28 RDT
      Process .ltx files, with -l implied

     version 0.11 2015-07-16 RDT
      Automatic autosp preprocessing. 

     version 0.10 2015-04-23 RDT
      Add -a option to preprocess using autosp

     version 0.9 2015-02-13 RDT
      Add an additional latex pass to resolve cross-references.
      Add -e0 option to dvips as given in musixdoc.tex
      Add -x option to call makeindex

     version 0.8 2014-05-18 RDT
      Add -g option

     version 0.7  2013-12-11 RDT
      Add -F fmt option

     version 0.6  2012-09-14 RDT
      Add -1 (one-pass [pdf][la]tex processing) option.

     version 0.5  2011-11-28 RDT
      Add -i (retain intermediate files) option.

     version 0.4  2011-04-30 RDT
       Allow multiple filenames (and options).
       Add -f (default) and -l (latex) options.

     version 0.3  2011-04-25 RDT
       Add -d (dvipdfm)  and -s (stop at dvi) options.

     version 0.2  2011-04-21 RDT
       Allow basename.tex as filename.
       Add -p option for pdfetex processing.
       Add standard -v -h options.
--]]
local orig_print = print

function usage()
  orig_print 
[[
Usage:  [texlua] musixtex.lua { option | basename[ .xml | .mtx | .pmx | .aspc | .tex | .ltx] } ...
        When no extension is given, extensions are tried in the above order 
        until a source file is found. Preprocessing goes 
           .xml - .pmx - .tex
           .mtx - .pmx - .tex
           .pmx - .tex
           .aspc - .tex 
        with the entry point determined by the filename extension.
        The normal route after preprocessing goes .tex - .dvi - .ps - .pdf, 
        but shorter routes are also available; see the options. 
        The default 3-pass processing route for .tex files is 
            etex - musixflx - etex.
        .ltx files, possibly after autosp preprocessing, are treated as latex 
        source and processed by latex (or pdflatex) rather than etex.
Options: -v, --version  version
         -h, --help   help
         -l  latex source; implied by .ltx extension
         -p  direct tex-pdf (pdftex etc)
         -F fmt  use fmt as the TeX processor
         -d  tex-dvi-pdf (using dvipdfm if -D not used)
         -D dvixx  use dvixx as the dvi processor
         -P ps2pdfxx  use ps2pdfxx as the Postscript processor
         -c  preprocess pmx file using pmxchords
         -m  stop at pmx
         -M prepmxx  use prepmxx as the mtx preprocessor
         -A autospx  use autospx as the aspc preprocessor
         -X pmxabx  use pmxabx as the pmx preprocessor
         -L xmlx use xmlx as the xml preprocessor
         -t  stop at tex/mid
         -s  stop at dvi
         -g  stop at ps
         -i  retain intermediate and log files
         -q  quiet mode (redirect screen logs, write summary on musixtex.log)
         -1  one-pass [pdf][la]tex processing
         -x  run makeindex
         -f  restore default processing
Four TeX engines are available via the -l and -p options. 
    etex      default
    latex     -l
    pdfetex   -p
    pdflatex  -l -p
If the -F option is used, options -l and -p need to be set if the engine 
name does not contain "latex" and "pdf" respectively. For example, the 
above four engines can be replaced by:
  -F "luatex --output-format=dvi" 
  -F "lualatex --output-format=dvi"
  -F "luatex" -p
  -F "lualatex" -p
]]
end

function whoami ()
  print("This is musixtex.lua version ".. VERSION .. ".")
end

function exists (filename, nolog)
  local f = io.open(filename, "r")
  if f then
    f:close()
    if not nolog then musixlog:write("Processing " .. filename,'\n' ) end
    return true
  else
    return false
  end
end

--   System commands for the various programs are mostly
--   set to nil if the step is to be omitted, which can be
--   tested by a simple "if" statement.
-- Exceptions:
--    'tex' is the command for processing a TeX file, but it is important
--       to know whether the user has explicitly specified an option that
--       affects this choice, in which case the automatic selection of the
--       command is restricted or disabled.
--         force_engine  "use the command provided"
--         override   records  when 'l' and/or 'p' options are in effect
--    'dvi' is the command for processing a DVI file, but there are two
--       reasons for not needing it and it is important for the automatic
--       selection of a TeX engine to know which is in effect. This is
--       possible by exploiting the the fact that Lua has two false values.
--         dvi == nil    "do not produce a DVI file" (but maybe PDF)
--         dvi == false  "do not process the DVI file" (but stop after TeX)

local dvips = "dvips -e0"  
-- option -e0 suppresses dvips "feature" of adjusting location to align 
-- characters in words of text 

function defaults()
  xml2pmx = "xml2pmx"
  prepmx = "prepmx"
  pmx = "pmxab"
  autosp = "autosp"
  tex = "etex"
  musixflx = "musixflx"
  dvi = dvips
  ps2pdf = "ps2pdf"
  cleanup = true  -- clean up intermediate and log files
  index = false
  latex = false 
  force_engine = false -- indicates whether -F is specified
  override = ""   
  passes = 2
  quiet = ""     -- set by "-q"
end  
  
------------------------------------------------------------------------ 

if #arg == 0 then
  whoami()
  usage()
  os.exit(0)
end

-- Logging on `musixtex.log` everything that is printed, all os calls,
--   and warnings from other log files.

musixlog = io.open("musixtex.log","w")
if not musixlog then 
   print"!! Can't open files for writing in current directory, aborting"
   os.exit(1)
end

------------------------------------------------------------------------

print = function(...)
   orig_print(...)
   musixlog:write(...,"\n") -- only the first argument gets written!
end

local orig_remove = os.remove
remove = function(filename)  
  if exists(filename,"nolog") then
    musixlog:write("  removing ",filename,"\n") 
    return orig_remove(filename)
  end
end

local orig_execute = os.execute
execute = function(command)
  musixlog:write("  ",command,"\n") 
  return orig_execute(command .. quiet)
end

function report_warnings(filename,pattern)
  local log = io.open(filename)
  if not log then
    print("! No file "..filename)
    return
  end
  for line in log:lines() do
    if line:match(pattern) then
      musixlog:write("!  "..line,"\n")
    end
  end
end 

function report_error(filename)
  local log = io.open(filename)
  if not log then
    print("! No file "..filename)
    return
  end
  local trigger = false
  for line in log:lines() do
    if trigger and line:match"^!" then 
      -- report just the first error   
      break
    end
    trigger = trigger or line:match"^!"
    if trigger then
      io.stderr:write("!  "..line,"\n")
    end
  end
end 

function process_option(this_arg)
  if this_arg == "-v" or this_arg == "-version" or this_arg == "--version" then
    os.exit(0)
  elseif this_arg == "-h" or this_arg == "-help" or this_arg == "--help" then 
    usage()
    os.exit(0)
  elseif this_arg == "-l" then 
    latex = true
    override = override .. 'l'
  elseif this_arg == "-p" then
    dvi = nil
    override = override .. 'p'
  elseif this_arg == "-d" then
    dvi = "dvipdfm"; ps2pdf = nil
  elseif this_arg == "-D" then
    narg = narg+1
    dvi = arg[narg]
  elseif this_arg == "-c" then
    pmx = "pmxchords"
  elseif this_arg == "-F" then
    narg = narg+1
    tex = arg[narg]
    force_engine = true
  elseif this_arg == "-s" then
    dvi = false; ps2pdf = nil; protect.dvi = true
  elseif this_arg == "-g" then
    dvi = dvips; ps2pdf = nil; protect.ps = true
  elseif this_arg == "-i" then
    cleanup = false
  elseif this_arg == "-x" then
    index = true
  elseif this_arg == "-1" then
    passes = 1
  elseif this_arg == "-f" then
    defaults()
  elseif this_arg == "-t" then
    tex, dvi, ps2pdf = nil,nil,nil
    protect.tex = true
  elseif this_arg == "-m" then
    pmx, tex, dvi, ps2pdf = nil,nil,nil,nil
    protect.pmx = true
  elseif this_arg == "-M" then
    narg = narg+1
    prepmx = arg[narg]
  elseif this_arg == "-A" then
    narg = narg+1
    autosp = arg[narg]
  elseif this_arg == "-L" then
    narg = narg+1
    xml2pmx = arg[narg]
  elseif this_arg == "-q" then
    if not tempname then
      tempname = tempname or os.tmpname()
      print("Redirecting screen output to "..tempname)
    end
    if dvi == dvips then dvi = dvi .. " -q" end
    quiet = " >> " .. tempname
  elseif this_arg == "-P" then
    narg = narg+1
    ps2pdf = arg[narg]
  elseif this_arg == "-X" then
    narg = narg+1
    pmx = arg[narg]
  else
    print("! Unknown option "..this_arg.." ignored")
  end
end

function find_file(this_arg)
  basename, extension = this_arg:match"(.*)%.(.*)"  
  extensions = {["xml"] = true, ["mtx"] = true, ["pmx"] = true, ["aspc"] = true, ["tex"] = true, ["ltx"] = true}
  if extensions[extension] then
    return basename, extension
  end
  basename, extension  = this_arg, null
  for ext in ("xml,mtx,pmx,aspc,tex,ltx"):gmatch"[^,]+" do
    if exists (basename .. "." .. ext) then
      extension = ext
      break
    end
  end
  if extension == null then
    print("!! No file " .. basename .. ".[xml|mtx|pmx|aspc|tex|ltx]")
    exit_code = exit_code+1
    return
  end
  return basename, extension
end

function preprocess(basename,extension)
  if not (basename and extension) then return end
  if extension == "xml" then
    if execute(xml2pmx .. " " .. basename .. ".xml" .. " " .. basename .. ".pmx" ) == 0 then 
      extension = "pmx"
    else
      print ("!! xml2pmx preprocessing of " .. basename .. ".xml fails.")
      return
    end
  elseif extension == "mtx" then
    if execute(prepmx .. " " .. basename ) == 0 then
      extension = "pmx"
    else
      print ("!! prepmx preprocessing of " .. basename .. ".mtx fails.")
      return
    end
  end 
  if extension == "pmx" then
    local OK = true
    if pmx then 
      local code = execute(pmx .. " " .. basename)
      local pmxaerr = io.open("pmxaerr.dat", "r")
      if (not pmxaerr) then
        OK = false
        print("!! No pmx log file.")
      else
        extension = "tex"
        local linebuf = pmxaerr:read()
        local err = tonumber(linebuf)
        if (code == 0) then
          if err ~=0 then
            print ("!  pmx produced a warning on line "..err..
                " of "..basename..".pmx")
          end
        else
          OK = false
          print ("!! pmx processing of " .. basename .. ".pmx fails.") 
        end
        pmxaerr:close()
      end
    end
    if not OK then 
      exit_code = exit_code+1
      return
    end
  end
  if extension == "aspc" then
    if execute (autosp .. " " .. basename .. ".aspc" ) == 0 then
      if exists ( basename .. ".ltx")
        then extension = "ltx"
        else extension = "tex"
      end
    else
      print ("!! autosp preprocessing of " .. basename .. ".aspc fails.")
      exit_code = exit_code+1
      return
    end       
  end
  return extension
end

function tex_process(tex,basename,extension)
  if not (extension == "tex" or extension == "ltx") or not tex then return end
  remove(basename .. ".mx1")
  remove(basename .. ".mx2")
  local filename = basename .. "." ..extension
-- .ltx extension re-selects engine only for the current file, and only 
-- if default processing is plain TeX
  local latex = latex
  if extension == "ltx" then
    if not force_engine and not latex then
      if dvi then tex = "latex" else tex = "pdflatex" end
    end
    latex = true
  end
  if quiet ~= "" then
    tex = tex .. " -interaction batchmode"
  end
  local OK = (execute(tex .. " " .. filename) == 0)
  if passes ~= 1 then 
    OK = OK and (execute(musixflx .. " " .. basename) == 0)
      and (execute(tex .. " " .. filename) == 0)
    if latex and index then
      OK = OK and (execute("makeindex -q " .. basename) == 0)
              and (execute(tex .. " " .. filename) == 0)
      if exists (basename .. ".toc","nolog") then
        -- an extra line for the index will have been added
        OK = OK and (execute(tex .. " " .. filename) == 0)
        -- there is a tiny possibility that the extra line for the index
        -- has changed the pagination. If you feel this should not be
        -- ignored, here are the possibilities:
        --   a. Do an extra TeX pass regardless.
        --   b. Provide a user option -4 to force the extra pass.
        --   c. Compare aux files to find out.
      end
    end
  end
  if (quiet ~= "") and not OK then
    report_error(basename..".log")
  end
  if OK and latex then 
    report_warnings(basename..".log","LaTeX.*Warning")
  end
  if dvi then OK = OK and (execute(dvi .. " " .. basename) == 0) end
  if dvi and ps2pdf then
    OK = OK and (execute(ps2pdf .. " " .. basename .. ".ps") == 0)
    if OK then 
      print(basename .. ".pdf generated by " .. ps2pdf .. ".")
    end
  end
  if not OK then
    print("!! Processing of " .. filename .. " fails.\n")
    exit_code = exit_code+1
  end
end

---- Report version information on musixtex.log

--  File names and message signatures to be looked for in a TeX log file.
logtargets =  {
mtxtex = {"mtx%.tex","mtxTeX"},
mtxlatex = {"mtxlatex%.sty","mtxLaTeX"},
pmxtex = {"pmx%.tex","PMX"},
musixtex = {"musixtex%.tex","MusiXTeX"},
musixltx = {"musixltx%.tex","MusiXLaTeX"},
musixlyr = {"musixlyr%.tex","MusiXLYR"}
}

-- Signatures of messages displayed on standard output by programs 
capturetargets = {
MTx = "This is (M%-Tx.->)",
PMX = "This is (PMX[^\n]+)",
pdftex = "This is (pdfTeX[^\n]+)",
musixflx = "Musixflx%S+",
autosp = "This is autosp.*$",
index = "autosp,MTx,PMX,musixflx,pdftex"
}

function report_texfiles(logname)
  local log = logname and io.open(logname)
  if not log then return end
  local lines = 
    {"---   TeX files actually included according to "..logname.."   ---"}
  log = log:read"*a"
-- The following pattern matches filenames input by TeX, even if
-- interrupted by a line break. It may include the first word of
-- an \immediate\write10 message emitted by the file.
  for pos,filename in log:gmatch"%(()(%.?[/]%a[%-/%.%w\n]+)" do
    local hit
    repeat  
      local oldfilename = filename
      filename = oldfilename:match"[^\n]+"   -- up to next line break
      hit = io.open(filename)                -- success if the file exists
      if hit then break end
      filename = oldfilename:gsub("\n","",1) -- remove line break
    until filename==oldfilename 
    if hit then
      for target,sig in pairs(logtargets) do
        if filename:match(sig[1]) then
          local i,j = log:find(sig[2].."[^\n]+",pos)          
          if j then lines[#lines+1] = filename.."\n  "..log:sub(i,j) end
        end
      end
    end
  end
  return table.concat(lines,'\n').."\n"
end  

function report_versions(tempname)
  if not tempname then return end  -- only available with -q
  local logs = io.open(tempname)
  if not logs then 
     musixlog:write ("No version information: could not open "..tempname)
     return
  end
  local versions = {}
  musixlog:write("---   Programs actually executed according to "..tempname.."   ---\n")
  for line in logs:lines() do
    for target in capturetargets.index:gmatch"[^,]+" do
      if not versions[target] then
        local found = line:match(capturetargets[target]) 
        if found then
          versions[target] = found
          musixlog:write(found,"\n")
        end 
      end
    end
  end
  logs:close()
  return
end

------------------------------------------------------------------------

whoami()

defaults()

exit_code = 0
narg = 1
protect = {}  -- extensions not to be deleted, even if cleanup requested

repeat
  this_arg = arg[narg]
  if this_arg:match"^%-" then process_option(this_arg)
  else
    basename, extension = find_file(this_arg)  -- nil,nil if not found
    if tex then -- tex output enabled, now select engine
      if tex:match"pdf" then dvi = nil end
      if not dvi then ps2pdf = nil end
      -- .ltx extension will be taken into account later, in `process`
      -- deduce tex/latex from current engine name if -l is not specified
      if not override:match"l" then latex = tex:match"latex" end 
      if not force_engine then -- select appropriate default engine
        if latex then 
          if dvi==nil then tex = "pdflatex" else tex = "latex" end
        else 
          if dvi==nil then tex = "pdfetex" else tex = "etex" end
        end  
      end
    end
    extension = preprocess(basename, extension)
    tex_process(tex,basename,extension)
    if basename and io.open(basename..".log") then -- to be printed later
      versions = report_texfiles(basename..".log")
    end
    if basename and cleanup then
      remove("pmxaerr.dat")
      for ext in ("mx1,mx2,dvi,ps,idx,log,ilg,pml"):gmatch"[^,]+" do
        if not protect[ext] then remove(basename.."."..ext)
        end
      end
    end
    protect = {}
  end 
  narg = narg+1
until narg > #arg 

if versions then musixlog:write(versions) end
report_versions(tempname)
musixlog:close()
os.exit( exit_code )