/*
 * turbomole.go, part of gochem.
 *
 *
 * Copyright 2012 Raul Mera <rmera{at}chemDOThelsinkiDOTfi>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 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 Lesser General
 * Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 *
 *
 * Gochem is developed at the laboratory for instruction in Swedish, Department of Chemistry,
 * University of Helsinki, Finland.
 *
 *
 */

//The TM handler implementation differs from the rest in that it uses several TM programs
// (define, x2t, t2x, cosmoprep) in order to prepare the input and retrieve results.
//Because of this, the programs using this handler will not work if TM is not installed.
//The handler has been made to work with TM7.

package qm

import (
	"bufio"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"strconv"
	"strings"

	chem "github.com/rmera/gochem"
	v3 "github.com/rmera/gochem/v3"
)

// TMHandle is the representation of a Turbomole (TM) calculation
// This imlpementation supports only singlets and doublets.
type TMHandle struct {
	defmethod    string
	defbasis     string
	defauxbasis  string
	defgrid      int
	previousMO   string
	command      string
	cosmoprepcom string
	inputname    string
	gimic        bool
	marij        bool
	dryrun       bool
}

// Creates and initializes a new instance of TMRuner, with values set
// to its defaults.
func NewTMHandle() *TMHandle {
	run := new(TMHandle)
	run.SetDefaults()
	return run
}

const noCosmoPrep = "goChem/QM: Unable to run cosmoprep"

//TMHandle methods

// SetName sets the name of the subdirectory, in the current directory
// where the calculation will be ran
func (O *TMHandle) Name(name ...string) string {
	ret := O.inputname
	if len(name) > 0 && name[0] != "" {
		O.inputname = name[0]
		ret = name[0]
	}
	return ret

}

// SetMARIJ sets the multipole acceleration
func (O *TMHandle) SetMARIJ(state bool) {
	O.marij = state
}

// SetDryRun sets the flag to see this is a dry run or
// if define will actually be run.
func (O *TMHandle) SetDryRun(dry bool) {
	O.dryrun = dry
}

// SetCommand doesn't do anything, and it is here only for compatibility.
// In TM the command is set according to the method. goChem assumes a normal TM installation.
func (O *TMHandle) SetCommand(name string) {
	//Does nothing again
}

// SetCommand doesn't do anything, and it is here only for compatibility.
// In TM the command is set according to the method. goChem assumes a normal TM installation.
func (O *TMHandle) SetCosmoPrepCommand(name string) {
	O.cosmoprepcom = name
}

// SetDefaults sets default values for TMHandle. default is an optimization at
//
//	TPSS-D3 / def2-SVP
//
// Defaults are not part of the API, they might change as new methods appear.
func (O *TMHandle) SetDefaults() {
	O.defmethod = "B97-3c"
	O.defbasis = "def2-mTZVP"
	O.defgrid = 4
	O.defauxbasis = "def2-mTZVP"
	O.command = "ridft"
	O.marij = false  //Apparently marij can cause convergence problems
	O.dryrun = false //define IS run by default.
	O.inputname = "gochemturbo"
	O.cosmoprepcom = "cosmoprep"
}

// addMARIJ adds the multipole acceleration if certain conditions are fullfilled:
// O.marij must be true
// The RI approximation must be in use
// The system must have more than 20 atoms
// The basis set cannot be very large (i.e. it can NOT be quadruple-zeta, tzvpp, or basis with diffuse functions)
func (O *TMHandle) addMARIJ(defstring string, atoms chem.AtomMultiCharger, Q *Calc) string {
	if !O.marij {
		return defstring
	}
	if strings.Contains(strings.ToLower(Q.Basis), "def2") && strings.HasSuffix(Q.Basis, "d") { //Rappoport basis
		return defstring
	}
	if strings.Contains(Q.Basis, "cc") && strings.Contains(Q.Basis, "aug") { //correlation consistent with diffuse funcs.
		return defstring
	}
	if strings.Contains(strings.ToLower(Q.Basis), "qz") { //both cc-pVQZ and def2-QZVP and QZVPP
		return defstring
	}

	if strings.Contains(strings.ToLower(Q.Basis), "def2-tzvpp") { //This is less clear but just in case I won't add MARIJ for tzvpp
		return defstring
	}
	//I Have no idea what the MARIJ string was supposed to be. For now the method doesn't work. Remove this if you fix the code
	//below, and uncoment it.
	//	if Q.RI && atoms.Len() >= 20 {
	//		defstring = fmt.Sprintf("%s%s\n\n")
	//	}
	return defstring
}

func ReasonableSetting(k string, Q *Calc) string {
	if strings.Contains(k, "$scfiterlimit") {
		k = "$scfiterlimit   100\n"
	}

	if strings.Contains(k, "$maxcor    500 MiB  per_core") {
		k = "$maxcor    3000 MiB  per_core\n"
	}
	if strings.Contains(k, "$ricore      500") {
		k = "$ricore      1000\n"
	}
	if Q.SCFConvHelp > 1 && strings.Contains(k, "$scfdamp") {
		k = "$scfdamp start=10 step=0.005 min=0.5\n"

	}
	if Q.SCFTightness > 1 && strings.Contains(k, "$scfconv") {
		k = "$scfconv  8\n"
	}

	return k
}

/*
//Replaces the regular expression regex in the TM control file by replacement
func (O *TMHandle) ReplaceInControl(regex, replacement string) error {
    	//NOTE: This has repeated code. Refactor
	re, err := regexp.Compile(regex)
	if err != nil {
		return fmt.Errorf("ReplaceInControl: Replacement failed: %s", err.Error())
	}

	path, err := os.Getwd()
	if err != nil {
		return err
	}
	dir := strings.Split(path, "/")
	if dir[len(dir)-1] != O.inputname {
		defer os.Chdir("../")
		os.Chdir(O.inputname)
	}

	f, err := os.Open("control")
	if err != nil {
		return Error{ErrCantInput, Turbomole, O.inputname, "", []string{"os.Open", "addtoControl"}, true}
	}
	lines := make([]string, 0, 200) //200 is just a guess for the number of lines in the control file
	c := bufio.NewReader(f)
	for err == nil {
		var line string
		line, err = c.ReadString('\n')
		lines = append(lines, line)
	}
	f.Close() //I cant defer it because I need it closed now.
	out, err := os.Create("control")
	if err != nil {
		return Error{ErrCantInput, Turbomole, O.inputname, "", []string{"os.Create", "addtoControl"}, true}
	}
	defer out.Close()
	for _, i := range lines {
		k := i
		if re.MatchString(i) {
			k = replacement
		}

		if _, err := fmt.Fprintf(out, k); err != nil {
			return Error{ErrCantInput, Turbomole, O.inputname, "", []string{"fmt.Fprintf", "ReplaceInControl"}, true}
		}
	}
	return nil
}
*/

//Adds the text strings given before or after the first line containing the where[0] string. By default the "target" is "$symmetry"
func (O *TMHandle) AddToControl(toappend []string, before bool, where ...string) error {
	c, err := os.Getwd()
	if err != nil {
		return err
	}
	dir := strings.Split(c, "/")
	if dir[len(dir)-1] != O.inputname {
		defer os.Chdir("../")
		os.Chdir(O.inputname)
	}
	return O.addToControl(toappend, nil, before, where...)
}

// Adds all the strings in toapend to the control file, just before the $symmetry keyword
// or just before the keyword specified in where.
func (O *TMHandle) addToControl(toappend []string, Q *Calc, before bool, where ...string) error {
	f, err := os.Open("control")
	if err != nil {
		return Error{ErrCantInput, Turbomole, O.inputname, "", []string{"os.Open", "addtoControl"}, true}
	}
	lines := make([]string, 0, 200) //200 is just a guess for the number of lines in the control file
	c := bufio.NewReader(f)
	for err == nil {
		var line string
		line, err = c.ReadString('\n')
		lines = append(lines, line)
	}
	f.Close() //I cant defer it because I need it closed now.
	out, err := os.Create("control")
	if err != nil {
		return Error{ErrCantInput, Turbomole, O.inputname, "", []string{"os.Create", "addtoControl"}, true}
	}
	defer out.Close()
	var k string
	target := "$symmetry"
	if len(where) > 0 {
		target = where[0]
	}
	for _, i := range lines {
		k = i //May not be too efficient
		if Q != nil {
			k = ReasonableSetting(k, Q)
		}
		if strings.Contains(i, target) {
			if !before {
				if _, err := fmt.Fprintf(out, k); err != nil {
					return Error{ErrCantInput, Turbomole, O.inputname, "", []string{"fmt.Fprintf", "addtoControl"}, true}

				}
			}
			for _, j := range toappend {
				if _, err := fmt.Fprintf(out, j+"\n"); err != nil {
					return Error{ErrCantInput, Turbomole, O.inputname, "", []string{"fmt.Fprintf", "addtoControl"}, true}
				}
			}
		}
		if !strings.Contains(i, target) || before {
			if _, err := fmt.Fprintf(out, k); err != nil {
				return Error{ErrCantInput, Turbomole, O.inputname, "", []string{"fmt.Fprintf", "addtoControl"}, true}

			}
		}
	}
	return nil
}

func (O *TMHandle) addCosmo(epsilon float64) error {
	//The ammount of newlines is wrong, must fix
	cosmostring := "" //a few newlines before the epsilon
	if epsilon == 0 {
		return nil
	}
	cosmostring = fmt.Sprintf("%s%3.1f\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nr all b\n*\n\n\n\n\n\n", cosmostring, epsilon)
	def := exec.Command(O.cosmoprepcom)
	pipe, err := def.StdinPipe()
	if err != nil {
		return Error{noCosmoPrep, Turbomole, O.inputname, err.Error(), []string{"exec.StdinPipe", "addCosmo"}, true}
	}
	defer pipe.Close()
	pipe.Write([]byte(cosmostring))
	if err := def.Run(); err != nil {
		return Error{noCosmoPrep, Turbomole, O.inputname, err.Error(), []string{"exec.Run", "addCosmo"}, true}

	}
	return nil

}

func (O *TMHandle) addBasis(basisOrEcp string, basiselems []string, basis, defstring string) string {
	if basiselems == nil { //no atoms to add basis to, do nothing
		return defstring
	}
	for _, elem := range basiselems {
		defstring = fmt.Sprintf("%s%s \"%s\" %s\n", defstring, basisOrEcp, strings.ToLower(elem), basis)
	}
	return defstring
}

// modifies the coord file such as to freeze the atoms in the slice frozen.
func (O *TMHandle) addFrozen(frozen []int) error {
	f, err := os.Open("coord")
	if err != nil {
		return Error{noCosmoPrep, Turbomole, O.inputname, err.Error(), []string{"os.Open", "addFrozen"}, true}

	}
	lines := make([]string, 0, 200) //200 is just a guess for the number of lines in the coord file
	c := bufio.NewReader(f)
	for err == nil {
		var line string
		line, err = c.ReadString('\n')
		lines = append(lines, line)
	}
	f.Close() //I cant defer it because I need it closed now.
	out, err := os.Create("coord")
	defer out.Close()
	for key, i := range lines {
		if isInInt(frozen, key-1) {
			j := strings.Replace(i, "\n", " f\n", -1)
			if _, err := fmt.Fprintf(out, j); err != nil {
				return Error{noCosmoPrep, Turbomole, O.inputname, err.Error(), []string{"fmt.Fprintf", "addFrozen"}, true}

			}
		} else {
			if _, err := fmt.Fprintf(out, i); err != nil {
				return Error{noCosmoPrep, Turbomole, O.inputname, err.Error(), []string{"fmt.Fprintf", "addFrozen"}, true}

			}
		}
	}
	return nil
}

func copy2pipe(pipe io.ReadCloser, file *os.File, end chan bool) {
	io.Copy(file, pipe)
	end <- true
}

// BuildInput builds an input for TM based int the data in atoms, coords and C.
// returns only error.
// Note that at this point the interface does not support multiplicities different from 1 and 2.
// The number in atoms is simply ignored.
func (O *TMHandle) BuildInput(coords *v3.Matrix, atoms chem.AtomMultiCharger, Q *Calc) error {
	const noDefine = "goChem/QM: Unable to run define"
	const nox2t = "goChem/QM: Unable to run x2t"
	err := os.Mkdir(O.inputname, os.FileMode(0755))
	for i := 0; err != nil; i++ {
		if strings.Contains(err.Error(), "file exists") {
			O.inputname = fmt.Sprintf("%s%d", O.inputname, i)
			err = os.Mkdir(O.inputname, os.FileMode(0755))
		} else {
			return Error{"goChem/QM: Unable to build input", Turbomole, O.inputname, err.Error(), []string{"os.Mkdir", "BuildInput"}, true}
		}
	}
	_ = os.Chdir(O.inputname)
	defer os.Chdir("..")
	//Set the coordinates in a slightly stupid way.
	chem.XYZFileWrite("file.xyz", coords, atoms)
	x2t := exec.Command("x2t", "file.xyz")
	stdout, err := x2t.StdoutPipe()
	if err != nil {
		return Error{nox2t, Turbomole, O.inputname, err.Error(), []string{"exec.StdoutPipe", "BuildInput"}, true}
	}
	coord, err := os.Create("coord")
	if err != nil {
		return Error{nox2t, Turbomole, O.inputname, err.Error(), []string{"os.Create", "BuildInput"}, true}

	}
	if err := x2t.Start(); err != nil {
		return Error{nox2t, Turbomole, O.inputname, err.Error(), []string{"exec.Start", "BuildInput"}, true}

	}
	//	var end chan bool
	//	go copy2pipe(stdout, coord, end)
	//	<-end
	io.Copy(coord, stdout)
	coord.Close()                           //not defearable
	defstring := "\n\n\na coord\nired\n*\n" //reduntant internals
	if Q.CartesianOpt {
		defstring = "\n\n\na coord\n*\nno\n"
	}
	if atoms == nil || coords == nil {
		return Error{ErrMissingCharges, Turbomole, O.inputname, "", []string{"BuildInput"}, true}
	}
	if Q.Basis == "" {
		log.Printf("no basis set assigned for TM calculation, will used the default %s, \n", O.defbasis)
		Q.Basis = O.defbasis
	}
	//there is another check for these methods below. I should try to make it that it only checks for this
	//once.
	if Q.Method == "r2scan-3c" {
		Q.Basis = "def2-mTZVPP"
		Q.RI = true
	} else if Q.Method == "b97-3c" {
		Q.Basis = "def2-mTZVP"
		Q.RI = true
	} else if Q.Method == "hf-3c" {
		Q.Basis = "minix"
		Q.RI = false
	}
	defstring = defstring + "b all " + Q.Basis + "\n"
	if Q.LowBasis != "" && len(Q.LBElements) > 0 {
		defstring = O.addBasis("b", Q.LBElements, Q.LowBasis, defstring)
	}
	if Q.HighBasis != "" && len(Q.HBElements) > 0 {
		defstring = O.addBasis("b", Q.HBElements, Q.HighBasis, defstring)
	}
	//Manually adding ECPs seem to be problematic, so I don't advise to do so.
	if Q.ECP != "" && len(Q.ECPElements) > 0 {
		defstring = O.addBasis("ecp", Q.ECPElements, Q.ECP, defstring)
	}
	defstring = defstring + "\n*\n"
	//The following needs to be added because some atoms (I haven't tried so many, but
	//so far only copper) causes define to ask an additional question. If one doesn't add "y\n"
	//for each of those questions, the whole input for define will be wrong.
	stupid := ""
	stupidatoms := "Zn Cu" //if you want to add more stupid atoms just add then to the string: "Cu Zn"
	for i := 0; i < atoms.Len(); i++ {
		if stupidatoms == "" {
			break
		}
		if strings.Contains(stupidatoms, atoms.Atom(i).Symbol) {
			stupidatoms = strings.Replace(stupidatoms, atoms.Atom(i).Symbol, "", -1)
			stupid = stupid + "y\n"
		}
	}
	//Here we only produce singlet and doublet states (sorry). I will most certainly *not* deal with the "joys"
	//of setting other multiplicities in define.
	defstring = fmt.Sprintf("%seht\n%sy\ny\n%d\n\n\n\n\n", defstring, stupid, atoms.Charge()) //I add one additional "y\n"
	method, ok := tMMethods[strings.ToLower(Q.Method)]
	if !ok {
		fmt.Fprintf(os.Stderr, "no method assigned for TM calculation, will used the default %s, \n", O.defmethod)
		Q.Method = O.defmethod
		Q.RI = true
	} else {
		Q.Method = method
	}
	//We only support HF and DFT
	O.command = "dscf"
	if Q.Method != "hf" && Q.Method != "hf-3c" {
		grid := ""
		if Q.Grid != 0 && Q.Grid <= 7 {
			grid = fmt.Sprintf("grid\n m%d\n", Q.Grid)
		} else {

			grid = fmt.Sprintf("grid\n m%d\n", O.defgrid)
		}
		defstring = defstring + "dft\non\nfunc " + Q.Method + "\n" + grid + "*\n"
		if Q.RI {
			mem := 500
			if Q.Memory != 0 {
				mem = Q.Memory
			}
			defstring = fmt.Sprintf("%sri\non\nm %d\n*\n", defstring, mem)
			O.command = "ridft"
		}
	}
	defstring = O.addMARIJ(defstring, atoms, Q)
	defstring = defstring + "*\n"
	log.Println(defstring)

	//set the frozen atoms (only cartesian constraints are supported)
	if err := O.addFrozen(Q.CConstraints); err != nil {
		return errDecorate(err, "BuildInput")
	}
	if O.dryrun {
		return nil
	}
	def := exec.Command("define")
	pipe, err := def.StdinPipe()
	if err != nil {
		return Error{noDefine, Turbomole, O.inputname, err.Error(), []string{"exec.StdinPipe", "BuildInput"}, true}
	}
	defer pipe.Close()
	pipe.Write([]byte(defstring))
	if err := def.Run(); err != nil {
		return Error{noDefine, Turbomole, O.inputname, err.Error(), []string{"exec.Run", "BuildInput"}, true}
	}
	jc := jobChoose{}
	jc.opti = func() {
		O.command = "jobex -c 200"
		if Q.RI {
			O.command = O.command + " -ri"
			if Q.OptTightness >= 2 {
				O.command = O.command + "-gcart 4"

			}
		}
	}
	jc.forces = func() {
		O.command = "NumForce -central"
		if Q.RI {
			O.command = O.command + " -ri"
		}
		O.addToControl([]string{" weight derivatives"}, nil, false, "$dft")
	}
	Q.Job.Do(jc)

	//Now modify control
	args := make([]string, 1, 2)
	args[0], ok = tMDisp[Q.Dispersion]
	if !ok {
		fmt.Fprintf(os.Stderr, "Dispersion correction requested %s not supported, will used the default: D3, \n", Q.Dispersion)
		args[0] = "$disp3"
	}

	if Q.Method == "hf-3c" {
		args[0] = "$disp3 -bj -func hf-3c"
	}
	if Q.Method == "b97-3c" {
		args[0] = "$disp3 -bj -abc"
	}

	if strings.Contains(Q.Method, "r2scan") { // both r2scan and r2scan-3c get the extra grid
		if err := O.addToControl([]string{"    radsize   8"}, nil, false, "gridsize"); err != nil {
			return errDecorate(err, "BuildInput")
		}
		if Q.Method == "r2scan-3c" {
			args[0] = "$disp4"
		}

	}
	if Q.Gimic {
		O.command = "mpshift"
		args = append(args, "$gimic")
	}
	if err := O.addToControl(args, Q, true); err != nil {
		return errDecorate(err, "BuildInput")
	}

	//Finally the cosmo business.
	err = O.addCosmo(Q.Dielectric)
	if err != nil {
		return errDecorate(err, "BuildInput")
	}
	return nil

}

var tMMethods = map[string]string{
	"hf":        "hf",
	"hf-3c":     "hf-3c",
	"b3lyp":     "b3-lyp",
	"b3-lyp":    "b3-lyp",
	"pbe":       "pbe",
	"tpss":      "tpss",
	"tpssh":     "tpssh",
	"bp86":      "b-p",
	"b-p":       "b-p",
	"blyp":      "b-lyp",
	"b-lyp":     "b-lyp",
	"b97-3c":    "b97-3c",
	"r2scan-3c": "r2scan-3c",
	"r2scan":    "r2scan",
	"pw6b95":    "pw6b95",
}

var tMDisp = map[string]string{
	"":       "",
	"nodisp": "",
	"D":      "$olddisp",
	"D2":     "$disp2",
	"D3":     "$disp3",
	"D3BJ":   "$disp3 -bj",
	"D4":     "$disp4",
	"D3abc":  "$disp3 -bj -abc",
}

func (O *TMHandle) PreOpt(wait bool) error {
	command := exec.Command("sh", "-c", "jobex -level xtb -c 1000 > jobexpreopt.out")
	var err error
	os.Chdir(O.inputname)
	defer os.Chdir("..")
	if wait == true {
		err = command.Run()
	} else {
		err = command.Start()
	}
	if err != nil {
		err = Error{ErrNotRunning, Turbomole, O.inputname, err.Error(), []string{"exec.Run/Start", "Run"}, true}
		return err

	}
	return nil
}

// Run runs the command given by the string O.command
// it waits or not for the result depending on wait.
// This is a Unix-only function.
func (O *TMHandle) Run(wait bool) error {
	os.Chdir(O.inputname)
	defer os.Chdir("..")
	var err error
	filename := strings.Fields(O.command)
	//fmt.Println("nohup " + O.command + " > " + filename[0] + ".out")
	command := exec.Command("sh", "-c", "nohup "+O.command+" >"+filename[0]+".out")
	if wait == true {
		err = command.Run()
	} else {
		err = command.Start()
	}
	if err != nil {
		err = Error{ErrNotRunning, Turbomole, O.inputname, err.Error(), []string{"exec.Run/Start", "Run"}, true}

	}
	return err
}

// Energy returns the energy from the corresponding calculation, in kcal/mol.
func (O *TMHandle) Energy() (float64, error) {
	os.Chdir(O.inputname)
	defer os.Chdir("..")
	f, err := os.Open("energy")
	if err != nil {
		return 0, Error{ErrNoEnergy, Turbomole, O.inputname, err.Error(), []string{"os.Open", "Energy"}, true}
	}
	defer f.Close()
	fio := bufio.NewReader(f)
	line, err := getSecondToLastLine(fio)
	if err != nil {
		return 0, errDecorate(err, "Energy "+O.inputname)
	}
	en := strings.Fields(line)[1]
	energy, err := strconv.ParseFloat(en, 64)
	if err != nil {
		err = Error{ErrNoEnergy, Turbomole, O.inputname, err.Error(), []string{"strconv.ParseFloat", "Energy"}, true}
	}
	return energy * chem.H2Kcal, err
}

// OptimizedGeometry returns the coordinates for the optimized structure.
func (O *TMHandle) OptimizedGeometry(atoms chem.Atomer) (*v3.Matrix, error) {
	const not2x = "unable to run t2x "
	os.Chdir(O.inputname)
	defer os.Chdir("..")
	x2t := exec.Command("t2x")
	stdout, err := x2t.StdoutPipe()
	if err != nil {
		return nil, Error{ErrNoGeometry, Turbomole, O.inputname, not2x + err.Error(), []string{"exec.StdoutPipe", "OptimizedGeometry"}, true}
	}
	if err := x2t.Start(); err != nil {
		return nil, Error{ErrNoGeometry, Turbomole, O.inputname, not2x + err.Error(), []string{"exec.Start", "OptimizedGeometry"}, true}

	}
	mol, err := chem.XYZRead(stdout)
	if err != nil {
		return nil, errDecorate(err, "qm.OptimizedGeometry "+Turbomole+" "+O.inputname)
	}
	return mol.Coords[len(mol.Coords)-1], nil

}

// Gets the second to last line in a turbomole energy file given as a bufio.Reader.
// expensive on the CPU but rather easy on the memory, as the file is read line by line.
func getSecondToLastLine(f *bufio.Reader) (string, error) {
	prevline := ""
	line := ""
	var err error
	for {
		line, err = f.ReadString('\n')
		if err != nil {
			break
		}
		if !strings.Contains(line, "$end") {
			prevline = line
		} else {
			break
		}
	}
	return prevline, Error{err.Error(), Turbomole, "", "Unknown", []string{"getSecondToLastLine"}, true}
}
