"""Module iorefcalc of viperleed.calc.files.

Defines functions for reading and writing files relevant to the
reference calculation.
"""

__authors__ = (
    'Florian Kraushofer (@fkraushofer)',
    )
__copyright__ = 'Copyright (c) 2019-2024 ViPErLEED developers'
__created__ = '2020-08-19'
__license__ = 'GPLv3+'

import copy
import logging
import os

import fortranformat as ff
import numpy as np

from viperleed.calc.classes.beam import Beam
from viperleed.calc.files.beams import writeAUXBEAMS
from viperleed.calc.lib import leedbase
from viperleed.calc.lib.base import fortranContLine, splitMaxRight
from viperleed.calc.lib.version import Version

logger = logging.getLogger(__name__)


def combine_tensors(oripath=".", targetpath=".", buffer=0):
    """Combines Tensor files in 'oripath' into common files in 'targetpath'.
    The 'buffer' argument specified how much of a Tensor file can be stored in
    memory at a given time (in bytes). Set 0 to read the entire file at once
    (faster, but may be a problem if memory is limited)."""
    tensorfiles = [f for f in os.listdir(oripath) if f.startswith("T_")
                   and os.path.isfile(os.path.join(oripath, f))
                   and len(f.split("_")) == 3]
    tnum = {}
    for f in tensorfiles:
        try:
            num = int(f.split("_")[1])
            float(f.split("_")[2][:-2])
        except ValueError:
            continue   # not a real Tensor file
        if num not in tnum:
            tnum[num] = [f]
        else:
            tnum[num].append(f)
    if len(tnum) == 0:
        logger.error("Found no Tensor files to combine.")
        raise RuntimeError("No Tensor files found")
    for num in tnum:
        tlist = sorted(tnum[num], key=lambda x: float(x.split("_")[2][:-2]))
        name = os.path.join(targetpath, splitMaxRight(tlist[0], "_")[0])
        with open(name, "w") as wf:
            # having the 'if' this far outside duplicates code, but speeds
            #  up the loop
            if buffer == 0:
                for tf in tlist:
                    with open(os.path.join(oripath, tf), "r") as rf:
                        wf.write(rf.read())
                    try:
                        os.remove(os.path.join(oripath, tf))
                    except Exception:
                        logger.warning("Failed to delete file " + tf)
            else:
                for tf in tlist:
                    with open(os.path.join(oripath, tf), "r") as rf:
                        while True:
                            data = rf.read(buffer)
                            if data:
                                wf.write(data)
                            else:
                                break
                    try:
                        os.remove(os.path.join(oripath, tf))
                    except Exception:
                        logger.warning("Failed to delete file " + tf)
    return


def combine_fdout(oripath=".", targetpath="."):
    """Combines fd.out and amp.out files in oripath into a common file at
    targetpath."""
    for filetype in ("fd", "amp"):
        outlines = []
        startstr = filetype + "_"
        filelist = [f for f in os.listdir(oripath)
                    if os.path.isfile(os.path.join(oripath, f))
                    and f.startswith(startstr) and f.endswith("eV.out")]
        i = 0
        while i < len(filelist):
            try:
                float(filelist[i].split(startstr)[1].split("eV.out")[0])
                i += 1
            except ValueError:
                filelist.pop(i)
        if len(filelist) == 0:
            if filetype == "amp":
                return   # return without error, we don't need amp files
            logger.error("Found no fd.out files to combine.")
            raise RuntimeError("No fd.out files found")
        filelist.sort(key=lambda x: float(x.split(startstr)[1]
                                          .split("eV.out")[0]))
        nbeams = 0
        for f in filelist:
            with open(os.path.join(oripath, f), "r") as rf:
                lines = rf.readlines()
            lines = [s for s in lines if ".  CORRECT TERMINATION" not in s
                     and len(s.strip()) >= 1]
            if len(outlines) == 0:
                try:
                    nbeams = int(lines[1].strip().split()[0])
                except Exception:
                    logger.error("Failed to combine {}.out".format(filetype),
                                 exc_info=True)
                    raise
                outlines += lines
            else:
                outlines += lines[nbeams+2:]
            if (nbeams % 5) == 4:
                outlines += "\n"  # expects an empty line (fortran formatting)
            try:
                os.remove(os.path.join(oripath, f))
            except Exception:
                logger.warning("Failed to delete file " + f)
        try:
            with open(os.path.join(targetpath, (filetype + ".out")),
                      "w") as wf:
                wf.write("".join(outlines))
        except Exception:
            logger.error("Failed to write combined {}.out".format(filetype))
            raise
    return


def readFdOut(readfile="fd.out", for_error=False, ampfile="amp.out"):
    """Reads the fd.out file produced by the refcalc and returns a list of
    Beam objects. If 'ampfile' is set, will attempt to read complex amplitudes
    from the given file as well."""

    def parse_data(lines, which, filename, theobeams, nbeams):
        out_str = ""
        blocks = []
        for line in lines:
            if ".  CORRECT TERMINATION" in line:
                # sometimes superpos output is not in right line. sync issue?
                continue
            out_str += line + "\n"
            llist = line.split()
            if len(llist) == 0:
                continue  # skip empty lines
            try:
                float(llist[0])
            except ValueError:
                break  # end of data
            blocks.extend(llist)

        # Each block contains exactly nbeams+2 entries (double for amp)
        # reshape blocks so that each line corresponds to one block
        if which == "fd":
            blocks = np.reshape(blocks, (-1, nbeams+2))
        else:
            blocks = np.reshape(blocks, (-1, 2*nbeams+2))

        # and parse the data
        warned = False
        for block in blocks:
            if block[1] != "0.0001":
                if not for_error and not warned:
                    warned = True
                    logger.warning("File " + filename + "contains unexpected "
                                   "data for more than one structural "
                                   "variation. Only the first block was read.")
                continue
            en = float(block[0])
            values = [float(s) for s in block[2:]]
            for (j, beam) in enumerate(theobeams):
                if which == "fd":
                    beam.intens[en] = values[j]
                else:
                    beam.complex_amplitude[en] = complex(values[2*j],
                                                         values[2*j + 1])
        return out_str

    try:
        with open(readfile, 'r') as rf:
            filelines = [line[:-1] for line in rf.readlines()]
            rf.seek(0)
    except FileNotFoundError:
        logger.error("Error in readFdOut: file "+str(readfile)+" not found.")
        raise
    if filelines[0].startswith(" SOME ERROR OCCURED WHILE READING"):
        logger.error("File "+readfile+" reports error: "+filelines[0])
        raise Exception("File "+readfile+" reports error.")
    fdout = ""
    theobeams = []
    i = 1   # number lines as in text editor - be careful about indexing!
    nbeams = 1e10   # some large number, just to enter the while loop
    while i < nbeams+3:
        fdout += filelines[i-1] + "\n"
        llist = filelines[i-1].split()
        if i == 1:
            pass  # header - skip
        elif i == 2:
            nbeams = int(llist[0])
        else:
            theobeams.append(Beam((float(llist[1]), float(llist[2]))))
        i += 1

    # re-label the beams to get the correct number of characters and formatting
    mw = max([beam.getLabel()[1] for beam in theobeams])
    for beam in theobeams:
        beam.label = beam.getLabel(lwidth=mw)[0]

    fdout += parse_data(filelines[i-1:], "fd", readfile, theobeams ,nbeams)

    if ampfile and os.path.isfile(ampfile):
        with open(ampfile, 'r') as rf:
            amplines = [line[:-1] for line in rf.readlines()]
        # check header
        if any(amplines[i] != filelines[i] for i in range(1, nbeams + 2)):
            logger.warning("Failed to read " + ampfile + ": Header does not "
                           "match " + readfile)
            return theobeams, fdout
        # now read the rest
        parse_data(amplines[nbeams+2:], "amp", ampfile, theobeams, nbeams)
    if not theobeams:
        raise RuntimeError(f"No beams found in {readfile}")
    return theobeams, fdout


def writePARAM(sl, rp, lmax=-1):
    """Creats the contents of the PARAM file for the reference calculation.
    If no LMAX is passed, will use maximum LMAX from rp. Returns str."""
    if lmax == -1:
        lmax = rp.LMAX.max
    try:
        beamlist, beamblocks, beamN = writeAUXBEAMS(
            ivbeams=rp.ivbeams, beamlist=rp.beamlist, write=False)
    except Exception:
        logger.error("generatePARAM: Exception while getting data from "
                     "writeAUXBEAMS")
        raise

    # define Clebsh-Gordon coefficient tables:
    mnlmo = [1, 70, 264, 759, 1820, 3836, 7344, 13053, 21868, 34914, 53560,
             79443, 114492, 160952, 221408, 298809, 396492, 518206]
    mnlm = [1, 76, 284, 809, 1925, 4032, 7680, 13593, 22693, 36124, 55276,
            81809, 117677, 165152, 226848, 305745, 405213, 529036]

    # start generating output
    output = ('C  Dimension statements for Tensor LEED reference calculation, '
              '\nC  version v1.2\n\n')
    output += 'C  1. lattice symmetry\n\n'
    nl1, nl2 = leedbase.get_superlattice_repetitions(rp.SUPERLATTICE)
    ideg = 2  # any 2D point grid is at least 2fold symmetric
    # if sl.planegroup in ['p2','pmm','pmg','pgg','cmm','rcmm']:
    #     ideg = 2
    if sl.planegroup in ['p3', 'p3m1', 'p31m']:
        ideg = 3
    elif sl.planegroup in ['p4', 'p4m', 'p4g']:
        ideg = 4
    elif sl.planegroup in ['p6', 'p6m']:
        ideg = 3
        # should be 6, but according to TensErLEED fortran comments,
        #   3 works better
    output += ('      PARAMETER (MIDEG='+str(ideg)+',MNL1='+str(nl1)
               + ',MNL2='+str(nl2)+')\n')
    output += '      PARAMETER (MNL = MNL1*MNL2)\n'
    output += '\nC  2. General calculational quantities\n\n'
    output += '      PARAMETER (MKNBS = '+str(beamblocks)+')\n'
    output += '      PARAMETER (MKNT =  '+str(beamN)+')\n'
    output += ('      PARAMETER (MNPUN = '+str(len(beamlist))+', MNT0 = '
               + str(len(beamlist))+')\n')
    output += ('      PARAMETER (MNPSI = '+str(len(rp.phaseshifts))+', MNEL = '
               + str(len(rp.phaseshifts[0][1]))+')\n')
    output += '      PARAMETER (MLMAX = '+str(lmax)+')\n'
    output += ('      PARAMETER (MNLMO = '+str(mnlmo[lmax-1])+', MNLM = '
               + str(mnlm[lmax-1])+')\n')
    output += '\nC  3. Parameters for (3D) geometry within (2D) unit mesh\n\n'
    output += '      PARAMETER (MNSITE  = '+str(len(sl.sitelist))+')\n'
    output += '      PARAMETER (MNLTYPE = '+str(sl.n_layers)+')\n'
    mnbrav = 0
    mnsub = 0
    mnstack = 0
    if sl.bulkslab is None:
        sl.make_bulk_slab(rp)
    for layer in sl.non_bulk_layers:
        mnstack += 1
        if layer.n_atoms == 1:
            mnbrav += 1
        if layer.n_atoms > mnsub:
            mnsub = layer.n_atoms
    for i, layer in enumerate(sl.bulk_layers):
        if sl.bulkslab.layers[i].n_atoms == 1:
            mnbrav += 1
        if sl.bulkslab.layers[i].n_atoms > mnsub:
            mnsub = layer.n_atoms
    output += '      PARAMETER (MNBRAV  = '+str(mnbrav)+')\n'
    output += '      PARAMETER (MNSUB   = '+str(mnsub)+')\n'
    output += '      PARAMETER (MNSTACK = '+str(mnstack)+')\n'
    output += ('\nC  4. some derived quantities that must be treated '
               'explicitly (dummy dimensions for\n')
    output += 'C     special cases necessary\n\n'
    output += '      PARAMETER (MLMAX1=MLMAX+1)\n'
    output += '      PARAMETER (MLMMAX = MLMAX1*MLMAX1)\n\n'
    output += ('      PARAMETER (MNBRAV2 = '+('MNBRAV' if mnbrav > 0
                                              else '1')+')\n\n')
    output += ('      PARAMETER (MNCOMP= '+('MNLTYPE-MNBRAV' if mnsub > 1
                                            else '1 ')+')\n')
    output += ('      PARAMETER (MLMT  = '+('MNSUB*MLMMAX' if mnsub > 1
                                            else '1 ')+')\n')
    output += ('      PARAMETER (MNSUB2= '+('MNSUB * (MNSUB-1)/2' if mnsub > 1
                                            else '1 ')+')\n')
    output += ('      PARAMETER (MLMG  = '+('MNSUB2*MLMMAX*2' if mnsub > 1
                                            else '1 ')+')\n')
    output += ('      PARAMETER (MLMN  = '+('MNSUB * MLMMAX' if mnsub > 1
                                            else '1 ')+')\n')
    output += ('      PARAMETER (MLM2N = '+('2*MLMN' if mnsub > 1
                                            else '1 ')+')\n')
    output += ('      PARAMETER (MLMNI = '+('MNSUB*MLMMAX' if mnsub > 1
                                            else '1 ')+')\n')
    return output


def collectFIN(version=0.):
    """Combines AUXLATGEO, BEAMLIST, AUXNONSTRUCT, PHASESHIFTS, AUXBEAMS
    and AUXGEO into one string (input for refcalc), which it returns. Pass
    beamlist to avoid reading it again."""
    if version < Version('1.7.3'):
        filenames = ["AUXLATGEO", "BEAMLIST", "AUXNONSTRUCT", "PHASESHIFTS",
                     "AUXBEAMS", "AUXGEO"]
    else:
        filenames = ["AUXLATGEO", "BEAMLIST", "AUXNONSTRUCT", "PHASESHIFTS",
                     "AUXGEO"]
    fin = ""
    for fn in filenames:
        with open(fn, "r") as rf:
            fin += rf.read()
        if fin[-1] != "\n":
            fin += "\n"
    return fin


def writeAUXLATGEO(sl, rp):
    """Writes AUXLATGEO, which is part of the input FIN for the refcalc."""
    if rp.TL_VERSION < Version('1.7.0'):
        formatter = {'energies': ff.FortranRecordWriter('3F7.2'),
                     'uc': ff.FortranRecordWriter('2F7.4'),
                     'x_ase_wf': ff.FortranRecordWriter('2F7.4'),
                     }
        lj = 24  # ljust spacing
    else:
        formatter = {'energies': ff.FortranRecordWriter('3F9.2'),
                     'uc': ff.FortranRecordWriter('2F9.4'),
                     'x_ase_wf': ff.FortranRecordWriter('3F9.2'),
                     }
        lj = 30  # ljust spacing
    output = ''
    output += rp.systemName+' '+rp.timestamp+'\n'
    ens = [rp.THEO_ENERGIES.start,
           rp.THEO_ENERGIES.stop + 0.01,
           rp.THEO_ENERGIES.step]
    output += formatter['energies'].write(ens).ljust(lj) + 'EI,EF,DE\n'
    ucsurf = sl.ab_cell.T
    if sl.bulkslab is None:
        sl.make_bulk_slab(rp)
    ucbulk = sl.bulkslab.ab_cell.T
    output += formatter['uc'].write(ucbulk[0]).ljust(lj) + 'ARA1\n'
    output += formatter['uc'].write(ucbulk[1]).ljust(lj) + 'ARA2\n'
    if rp.TL_VERSION < Version('1.7.0'):
        output += ' 0.0    0.0             SS1\n'
        output += ' 0.0    0.0             SS2\n'
        output += ' 0.0    0.0             SS3\n'
        output += ' 0.0    0.0             SS4\n'
    output += formatter['uc'].write(ucsurf[0]).ljust(lj) + 'ARB1\n'
    output += formatter['uc'].write(ucsurf[1]).ljust(lj) + 'ARB2\n'
    if rp.TL_VERSION < Version('1.7.0'):
        output += ' 0.0    0.0             SO1\n'
        output += ' 0.0    0.0             SO2\n'
        output += ' 0.0    0.0             SO3\n'
        output += (formatter['x_ase_wf'].write([0.5, rp.V0_Z_ONSET]).ljust(lj)
                   + 'FR ASE\n')
    else:
        # Different parameters here in version 1.7! Previously in muftin
        output += (formatter['x_ase_wf'].write([rp.V0_IMAG, rp.V0_Z_ONSET,
                                                rp.FILAMENT_WF]).ljust(lj)
                   + 'V0i,ASE,WORKFN\n')
    try:
        with open('AUXLATGEO', 'w') as wf:
            wf.write(output)
    except Exception:
        logger.error("Failed to write AUXLATGEO file")
        raise
    logger.debug("Wrote to AUXLATGEO successfully.")
    return


def writeAUXNONSTRUCT(sl, rp):
    """Writes AUXNONSTRUCT, which is part of the input FIN for the refcalc."""
    try:
        beamnums, _, _ = writeAUXBEAMS(ivbeams=rp.ivbeams,
                                       beamlist=rp.beamlist,
                                       write=False)
    except Exception:
        logger.error("generatePARAM: Exception while getting data from "
                     "writeAUXBEAMS")
        raise
    if rp.TL_VERSION < Version('1.7.0'):
        formatter = {'tst': ff.FortranRecordWriter('F7.4'),
                     'incidence': ff.FortranRecordWriter('(F7.2, F6.1)'),
                     'beamnums': ff.FortranRecordWriter('15I4'),
                     'eps': ff.FortranRecordWriter('F7.4'),
                     'ints': ff.FortranRecordWriter('I3'),
                     }
    else:
        formatter = {'tst': ff.FortranRecordWriter('F11.8'),
                     'incidence': ff.FortranRecordWriter('2F9.4'),
                     'beamnums': ff.FortranRecordWriter('15I5'),
                     'eps': ff.FortranRecordWriter('F7.4'),
                     'ints': ff.FortranRecordWriter('I3'),
                     }
    output = ''

    output += (formatter['tst'].write([rp.ATTENUATION_EPS]).ljust(18)
               + '>>>>> ! <<<<<              TST\n')
    output += formatter['beamnums'].write(beamnums)+'\n'
    output += (formatter['incidence'].write([rp.THETA, rp.PHI]).ljust(45)
               + 'THETA FI\n')
    output += formatter['eps'].write([rp.BULKDOUBLING_EPS]).ljust(45) + 'EPS\n'
    output += (formatter['ints'].write([rp.BULKDOUBLING_MAX]).ljust(45)
               + 'LITER\n')
    output += formatter['ints'].write([rp.LMAX.max]).ljust(45) + 'LMAX\n'
    if rp.TL_VERSION >= Version('1.7.0'):
        # TODO: if phaseshifts are calculated differently, change format here
        output += (formatter['ints'].write([1]).ljust(45)
                   + 'PSFORMAT  1: Rundgren_v1.6; 2: Rundgren_v1.7\n')
    if rp.TL_VERSION >= Version('1.7.3'):
        output += (formatter['ints'].write([1]).ljust(45)
                   + 'IFORM - formatted input and output\n')
    try:
        with open('AUXNONSTRUCT', 'w') as wf:
            wf.write(output)
    except Exception:
        logger.error("Failed to write AUXNONSTRUCT file")
        raise
    logger.debug("Wrote to AUXNONSTRUCT successfully.")
    return


def writeAUXGEO(sl, rp):
    """Writes AUXGEO, which is part of the input FIN for the refcalc."""
    if rp.TL_VERSION < Version('1.7.0'):
        formatter = {'vibrocc': ff.FortranRecordWriter('2F7.4'),
                     'geo': ff.FortranRecordWriter('3F7.4'),
                     }
        lj = 26
    else:
        formatter = {'vibrocc': ff.FortranRecordWriter('2F9.4'),
                     'geo': ff.FortranRecordWriter('3F9.4'),
                     }
        lj = 32
    slab_c = sl.c_vector.copy()
    if rp.LAYER_STACK_VERTICAL:
        sl = copy.deepcopy(sl)
        sl.project_c_to_z()
        sl.update_layer_coordinates()
    output = ''
    output += ('---------------------------------------------------------'
               '----------\n')
    output += ('--- define chem. and vib. properties for different atomic '
               'sites ---\n')
    output += ('---------------------------------------------------------'
               '----------\n')
    i3 = ff.FortranRecordWriter('I3')
    ol = i3.write([len(sl.sitelist)]).ljust(lj)
    output += ol + 'NSITE: number of different site types\n'
    for i, site in enumerate(sl.sitelist):
        output += '-   site type '+str(i+1)+' ---\n'

        for el in sl.elements:
            # this reproduces the order of blocks contained in PHASESHIFTS:
            if el in rp.ELEMENT_MIX:
                chemelList = rp.ELEMENT_MIX[el]
            else:
                chemelList = [el]
            siteList = [s for s in sl.sitelist if s.el == el]
            for cel in chemelList:
                for s in siteList:
                    if s.isEquivalent(site):
                        occ, vib = site.occ[cel], site.vibamp[cel]
                        comment = ('Occ & VibAmp for '+cel+' in '+site.label
                                   + ' site')
                    else:
                        occ, vib = 0., 0.
                        comment = ''
                    try:
                        ol = formatter['vibrocc'].write([occ, vib]).ljust(lj)
                    except Exception:
                        logger.error(
                            "Exception while trying to write occupation / "
                            "vibrational amplitude for site " + site.label,
                            exc_info=True)
                    output += ol + comment + '\n'

    output += ('-----------------------------------------------------'
               '--------------\n')
    output += ('--- define different layer types                     '
               '           ---\n')
    output += ('-----------------------------------------------------'
               '--------------\n')
    ol = i3.write([sl.n_layers]).ljust(lj)
    output += ol + 'NLTYPE: number of different layer types\n'
    blayers = sl.bulk_layers
    layerOffsets = [np.zeros(3) for _ in range(sl.n_layers + 1)]
    if sl.bulkslab is None:
        sl.make_bulk_slab(rp)
    for i, layer in enumerate(sl.layers):
        output += '-   layer type '+str(i+1)+' ---\n'
        if layer.is_bulk:
            output += ('  2'.ljust(lj) + 'LAY = 2: layer type no. '
                       + str(i+1) + ' has bulk lateral periodicity\n')
        else:
            output += ('  1'.ljust(lj) + 'LAY = 1: layer type no. '
                       + str(i+1) + ' has overlayer lateral periodicity\n')
        if layer.is_bulk:
            bl = sl.bulkslab.layers[blayers.index(layer)]
            bulknums = {at.num for at in bl}
            bulkUnique = [at for at in layer if at.num in bulknums]
            natoms = len(bulkUnique)
            # sanity check: ratio of unit cell areas (given simply by
            #  SUPERLATTICE) should match ratio of written vs skipped atoms:
            arearatio = 1 / abs(np.linalg.det(rp.SUPERLATTICE))
            atomratio = len(bulkUnique) / layer.n_atoms
            if abs(arearatio - atomratio) > 1e-3:
                logger.warning(
                    'Ratio of bulk atoms inside/outside the bulk unit cell '
                    'differs from bulk/slab unit cell size ratio. This means '
                    'that the actual periodicity of the POSCAR does not match '
                    'the periodicity given in the SUPERLATTICE parameter. '
                    'Check SUPERLATTICE parameter and bulk symmetry!')
                rp.setHaltingLevel(2)
        else:
            natoms = layer.n_atoms
        ol = i3.write([natoms]).ljust(lj)
        output += ol+'number of Bravais sublayers in layer '+str(i+1)+'\n'
        if layer.is_bulk:
            writelist = bulkUnique
        else:
            writelist = layer.atlist
        writelist.sort(key=lambda atom: -atom.pos[2])
        for atom in writelist:
            writepos = atom.cartpos - atom.layer.cartori                        # TODO: .cartpos[2]. Issue #174
            ol = i3.write([sl.sitelist.index(atom.site)+1])
            if natoms != 1:
                ol += formatter['geo'].write([writepos[2],
                                              writepos[0], writepos[1]])
            else:
                # Bravais layers need to have coordinate (0., 0., 0.)
                #  -> store actual position for later, it will go into the
                #  interlayer vector
                ol += formatter['geo'].write([0., 0., 0.])
                layerOffsets[layer.num] += writepos
                layerOffsets[layer.num + 1] -= writepos
            ol = ol.ljust(lj)
            output += f'{ol}Atom N={atom.num} ({atom.el})\n'
    output += ('--------------------------------------------------------------'
               '-----\n')
    output += ('--- define bulk stacking sequence                             '
               '  ---\n')
    output += ('--------------------------------------------------------------'
               '-----\n')
    output += ('  0'.ljust(lj) + 'TSLAB = 0: compute bulk using layer '
               'doubling\n')

    # determine ASA
    if type(rp.BULK_REPEAT) == np.ndarray:
        bvectors_ASA = np.copy(rp.BULK_REPEAT) * np.array([1, 1, -1])
        if bvectors_ASA[2] < 0:
            bvectors_ASA = -bvectors_ASA
        bulkc = bvectors_ASA[2]
    else:
        bulkc = np.copy(rp.BULK_REPEAT)
        bvectors_ASA = -slab_c * bulkc/slab_c[2]
    # bulkc is now the repeat length along c. now correct for layer thickness:
    if rp.N_BULK_LAYERS == 2:
        bvectors_ASA[2] = bulkc - (blayers[1].cartbotz - blayers[0].cartori[2])  # TODO: .cartpos[2]. Issue #174
    else:
        bvectors_ASA[2] = bulkc - (blayers[0].cartbotz - blayers[0].cartori[2])  # TODO: .cartpos[2]. Issue #174

    # determine ASBULK - interlayer vector between bulk layers
    if rp.N_BULK_LAYERS == 2:
        # add layerOffsets for Bravais layers:
        bvectors_ASA += layerOffsets[blayers[0].num+1]
        # calculate ASBULK:
        bvectors_ASBULK = blayers[1].cartori - blayers[0].cartori               # TODO: .cartpos[2]. Issue #174
        bvectors_ASBULK[2] = blayers[1].cartori[2] - blayers[0].cartbotz
        bl2num = blayers[1].num
        # add layerOffsets for Bravais layers:
        bvectors_ASBULK -= layerOffsets[blayers[0].num+1]
        # correct ASA xy in case ASBULK already includes some:
        bvectors_ASA[:2] -= bvectors_ASBULK[:2]
    else:
        bl2num = blayers[0].num
        bvectors_ASBULK = bvectors_ASA

    ol = formatter['geo'].write([bvectors_ASA[2],
                                 bvectors_ASA[0], bvectors_ASA[1]]).ljust(lj)
    output += ol + 'ASA interlayer vector between different bulk units\n'
    ol = i3.write([blayers[0].num+1]).ljust(lj)
    output += (ol + 'top layer of bulk unit: layer type '+str(blayers[0].num+1)
               + '\n')
    ol = i3.write([bl2num+1]).ljust(lj)
    output += ol + 'bottom layer of bulk unit: layer type '+str(bl2num+1)+'\n'
    ol = formatter['geo'].write([bvectors_ASBULK[2], bvectors_ASBULK[0],
                                 bvectors_ASBULK[1]]).ljust(lj)
    output += (ol + 'ASBULK between the two bulk unit layers (may differ from '
               'ASA)\n')
    output += ('--------------------------------------------------------------'
               '-----\n')
    output += ('--- define layer stacking sequence and Tensor LEED output     '
               '  ---\n')
    output += ('--------------------------------------------------------------'
               '-----\n')
    nonbulk = sl.n_layers - rp.N_BULK_LAYERS
    if len(rp.TENSOR_OUTPUT) < nonbulk:    # check TENSOR_OUTPUT parameter
        if len(rp.TENSOR_OUTPUT) == 1:
            # interpret one value as concerning all
            rp.TENSOR_OUTPUT = rp.TENSOR_OUTPUT * nonbulk
        elif rp.TENSOR_OUTPUT:
            logger.warning(
                'Parameters TENSOR_OUTPUT is defined, but contains fewer '
                'values than there are non-bulk layers. Missing values '
                'will be set to 1.')
            rp.setHaltingLevel(1)
        for i in range(0, nonbulk-len(rp.TENSOR_OUTPUT)):
            rp.TENSOR_OUTPUT.append(1)
    if len(rp.TENSOR_OUTPUT) > nonbulk:
        logger.warning(
            'Parameters TENSOR_OUTPUT is defined, but contains more values '
            'than there are non-bulk layers. Excess values will be ignored.')
        rp.setHaltingLevel(1)
    ol = i3.write([sl.n_layers - rp.N_BULK_LAYERS]).ljust(lj)
    output += ol + 'NSTACK: number of layers stacked onto bulk\n'
    for layer in reversed(sl.non_bulk_layers):
        n = layer.num + 1
        v = sl.layers[n].cartori - layer.cartori
        v[2] = sl.layers[n].cartori[2] - layer.cartbotz                         # TODO: .cartpos[2]. Issue #174
        v = v + layerOffsets[n]   # add layerOffsets for Bravais layers
        ol = i3.write([n]) + formatter['geo'].write([v[2],
                                                     v[0], v[1]])
        output += (ol.ljust(lj) + 'layer '+str(n)+': layer type '+str(n)
                   + ', interlayer vector below\n')
        ol = i3.write([rp.TENSOR_OUTPUT[layer.num]]).ljust(lj)
        output += (ol + '0/1: Tensor output is required for this layer '
                   '(TENSOR_OUTPUT)\n')
        if rp.TENSOR_OUTPUT[layer.num] == 0:
            continue   # don't write the Tensor file names
        for i, atom in enumerate(layer):
            ol = f'T_{atom.num}'.ljust(lj)
            output += (ol + 'Tensor file name, current layer, sublayer '
                       + str(i+1) + '\n')
    output += ('--------------------------------------------------------------'
               '-----\n')
    output += ('--- end geometrical input                                     '
               '  ---\n')
    output += ('--------------------------------------------------------------'
               '-----\n')

    try:
        with open('AUXGEO', 'w') as wf:
            wf.write(output)
    except Exception:
        logger.error("Failed to write AUXGEO file")
        raise
    logger.debug("Wrote to AUXGEO successfully.")
    return


def writeMuftin(rp):
    """Writes a muftin.f file, which will be compiled for the refcalc."""
    output = """
C  Subroutine muftin contains explicit energy dependence of inner
C  potentials. The functional form should be left to the user entirely,
C  thus the subroutine is included in the input explicitly.

C  All quantities should be set in eV. Conversion to Hartree (atomic units)
C  is handled by ref-calc.f itself. Both the real and imaginary part should
C  be given as positive values (the program then handles the sign correctly).

      subroutine muftin(EEV,VO,VV,VPI,VPIS,VPIO)

C  global variable

C  EEV :  electron energy in the vacuum region
C  VO  :  difference between real part of inner potential in the bulk and in
C         topmost layer. Usually irrelevant -> set to zero.
C  VV  :  real part of the inner potential in the bulk.
C  VPI :  imaginary part of the inner potential.
C  VPIS:  in case different values for the imaginary part of the inner
C  VPIO:  potential for bulk and topmost layer were desired, VPIS would
C         correspond to the bulk value, VPIO would be the respective
C         value for the topmost layer. Usually, the effect is irrelevant,
C         i.e. both are set equal to VPI.

      real EEV,VO,VV,VPI,VPIS,VPIO

C  local variable

C  workfn: Work function of the LEED filament material. Theoretical predictions
C         for the inner potential usually have their energy zero fixed at the
C         Fermi level; the experimental energy scale (-> EEV) nominally
C         does the same, except for the fact that electrons need to overcome
C         the work function of the cathode first. Note that the this value is
C         thus formally determined by a LEED fit, yet don't trust its accuracy.

      real workfn

C  set work function of cathode
C  work function should be positive (added to exp. energy EEV)

      workfn = """
    output += str(round(rp.FILAMENT_WF, 4))+"\n"
    output += """
C  set real part of inner potential

"""
    if type(rp.V0_REAL) == list:
        oline = ("      VV = workfn-max({:.2f}, (({:.2f})+({:.2f})/sqrt("
                 "EEV+workfn+({:.2f}))))".format(*rp.V0_REAL))
    else:
        oline = "      VV = "+rp.V0_REAL
    output += fortranContLine(oline) + "\n"
    output += """
      write(6,*) workfn, EEV
      write(6,*) VV

c  set difference between bulk and overlayer inner potential

      VO = 0.

c  set imaginary part of inner potential - energy independent value used here

"""
    oline = "      VPI = "+str(rp.V0_IMAG)
    output += fortranContLine(oline) + "\n"
    output += """
C  set substrate / overlayer imaginary part of inner potential

"""
    oline = "      VPIS = "+str(rp.V0_IMAG)
    output += fortranContLine(oline) + "\n"
    oline = "      VPIO = "+str(rp.V0_IMAG)
    output += fortranContLine(oline) + "\n"
    output += """
      return
      end"""
    try:
        with open("muftin.f", "w") as wf:
            wf.write(output)
        logger.debug("Wrote to muftin.f successfully.")
    except Exception:
        logger.error("Exception while writing muftin.f file: ",
                     exc_info=True)
        raise
    return
