# -*- coding: utf-8 -*-
#
# Copyright (c) 2023, the cclib development team
#
# This file is part of cclib (http://cclib.github.io) and is distributed under
# the terms of the BSD 3-Clause License.

"""Parser for Gaussian output files"""


import re
import datetime
import numpy

from cclib.parser import data
from cclib.parser import logfileparser
from cclib.parser import utils
from cclib.parser.logfileparser import StopParsing


class Gaussian(logfileparser.Logfile):
    """A Gaussian 98/03 log file."""

    def __init__(self, *args, **kwargs):
        super().__init__(logname="Gaussian", *args, **kwargs)

    def __str__(self):
        """Return a string representation of the object."""
        return f"Gaussian log file {self.filename}"

    def __repr__(self):
        """Return a representation of the object."""
        return f'Gaussian("{self.filename}")'

    def normalisesym(self, label):
        """Use standard symmetry labels instead of Gaussian labels.

        To normalise:
        (1) If label is one of [SG, PI, PHI, DLTA], replace by [sigma, pi, phi, delta]
        (2) replace any G or U by their lowercase equivalent
        """
        # note: DLT must come after DLTA
        greek = [('SG', 'sigma'), ('PI', 'pi'), ('PHI', 'phi'),
                 ('DLTA', 'delta'), ('DLT', 'delta')]
        for k, v in greek:
            if label.startswith(k):
                tmp = label[len(k):]
                label = v
                if tmp:
                    label = f"{v}.{tmp}"

        ans = label.replace("U", "u").replace("G", "g")
        return ans

    # Use to map from the usual year suffixed to full years so package
    # versions can be sorted properly after parsing with
    # `packaging.parse.version`.
    YEAR_SUFFIXES_TO_YEARS = {
        '70': '1970',
        '76': '1976',
        '80': '1980',
        '82': '1982',
        '86': '1986',
        '88': '1988',
        '90': '1990',
        '92': '1992',
        '94': '1994',
        '98': '1998',
        '03': '2003',
        '09': '2009',
        '16': '2016',
    }

    def before_parsing(self):
        # Calculations use point group symmetry by default.
        self.uses_symmetry = True

        # Extract only well-formed numbers in scientific notation.
        self.re_scinot = re.compile(r'(\w*)=\s*(-?\d\.\d{2}D[+-]\d{2})')
        # Extract only well-formed numbers in traditional
        # floating-point format.
        self.re_float = re.compile(r'(\w*-?\w*)=\s*(-?\d+\.\d{10,})')

        # Flag for identifying Coupled Cluster runs.
        self.coupledcluster = False

        # Fragment number for counterpoise or fragment guess calculations
        # (normally zero).
        self.counterpoise = 0

        # Flag for identifying ONIOM calculations.
        self.oniom = False

        # Flag for identifying BOMD calculations.
        # These calculations have a back-integration algorithm so that not all
        # geometries should be kept.
        # We also add a "time" attribute to the parser.
        self.BOMD = False

        # Do we have high-precision polarizabilities printed from a
        # dedicated `polar` job? If so, avoid duplicate parsing.
        self.hp_polarizabilities = False
        
        # Used to estimate wall times from CPU times.
        self.num_cpu = 1
        
        # For detecting when excited states reset (because of a new level of theory,
        # or a new round in an optimisation etc).
        self.last_et = 0

    def after_parsing(self):
        # atomcoords are parsed as a list of lists but it should be an array
        if hasattr(self, "atomcoords"):
            self.atomcoords = numpy.array(self.atomcoords)

        # Correct the percent values in the etsecs in the case of
        # a restricted calculation. The following has the
        # effect of including each transition twice.
        if hasattr(self, "etsecs") and len(self.homos) == 1:
            new_etsecs = [[(x[0], x[1], x[2] * numpy.sqrt(2)) for x in etsec]
                          for etsec in self.etsecs]
            self.etsecs = new_etsecs

        if hasattr(self, "scanenergies"):
            self.scancoords = []
            if hasattr(self, 'optstatus') and hasattr(self, 'atomcoords'):
                converged_indexes = [x for x, y in enumerate(self.optstatus) if y & data.ccData.OPT_DONE > 0]
                self.scancoords = self.atomcoords[converged_indexes,:,:]
            elif hasattr(self, 'atomcoords'):
                self.scancoords = self.atomcoords

        if (hasattr(self, 'enthalpy') and hasattr(self, 'temperature')
                and hasattr(self, 'freeenergy')):
            self.set_attribute('entropy', (self.enthalpy - self.freeenergy) / self.temperature)

        # This bit is needed in order to trim coordinates that are printed a second time
        # at the end of geometry optimizations. Note that we need to do this for both atomcoords
        # and inputcoords. The reason is that normally a standard orientation is printed and that
        # is what we parse into atomcoords, but inputcoords stores the input (unmodified) coordinates
        # and that is copied over to atomcoords if no standard oritentation was printed, which happens
        # for example for jobs with no symmetry. This last step, however, is now generic for all parsers.
        # Perhaps then this part should also be generic code...
        # Regression that tests this: Gaussian03/cyclopropenyl.rhf.g03.cut.log
        if hasattr(self, 'optstatus') and len(self.optstatus) > 0:
            last_point = len(self.optstatus) - 1
            if hasattr(self, 'atomcoords'):
                self.atomcoords = self.atomcoords[:last_point + 1]
            if hasattr(self, 'inputcoords'):
                self.inputcoords = self.inputcoords[:last_point + 1]
            if hasattr(self, "rotconsts"):
                self.rotconsts = self.rotconsts[:last_point + 1]

        # If we parsed high-precision vibrational displacements, overwrite
        # lower-precision displacements in self.vibdisps
        if hasattr(self, 'vibdispshp'):
            self.vibdisps = self.vibdispshp
            del self.vibdispshp
        if hasattr(self, 'time'):
            self.time = [self.time[i] for i in sorted(self.time.keys())]
        if hasattr(self, 'energies_BOMD'):
            self.set_attribute('scfenergies',
              [self.energies_BOMD[i] for i in sorted(self.energies_BOMD.keys())])
        if hasattr(self, 'atomcoords_BOMD'):
            self.atomcoords= \
              [self.atomcoords_BOMD[i] for i in sorted(self.atomcoords_BOMD.keys())]

        # Gaussian prints 'forces' in input orientation unlike other values such as 'moments' or 'vibdisp'.
        # Therefore, we convert 'grads' to the values in standard orientation with rotation matrix.
        if hasattr(self, 'grads') and hasattr(self, 'inputcoords') and hasattr(self, 'atomcoords'):
            grads_std = []
            for grad, inputcoord, atomcoord in zip(self.grads, self.inputcoords, self.atomcoords):
                rotation = utils.get_rotation(numpy.array(inputcoord), numpy.array(atomcoord))
                grads_std.append(rotation.apply(grad))
            self.set_attribute('grads', numpy.array(grads_std))
        
        if hasattr(self, "ccenergy"):
            self.append_attribute("ccenergies", utils.convertor(self.ccenergy, "hartree", "eV"))
            del self.ccenergy
            
        # If we have cpu times but no wall-times, we can calculate the later based on the number of CPUs used.
        if "cpu_time" in self.metadata and "wall_time" not in self.metadata:
            self.metadata['wall_time'] = []
            for cpu_time in self.metadata['cpu_time']:
                self.metadata['wall_time'].append(cpu_time / self.num_cpu)
            

    def extract(self, inputfile, line):
        """Extract information from the file object inputfile."""

        # Extract the version number: "Gaussian 09, Revision D.01"
        # becomes "09revisionD.01".
        if line.strip() == "Cite this work as:":
            tokens = next(inputfile).split()
            self.metadata["legacy_package_version"] = ''.join([
                tokens[1][:-1],
                'revision',
                tokens[-1][:-1],
            ])

        # Extract the version number: "Gaussian 98: x86-Linux-G98RevA.11.3
        # 5-Feb-2002" becomes "1998+A.11.3", and "Gaussian 16:
        # ES64L-G16RevA.03 25-Dec-2016" becomes "2016+A.03".
        if "Gaussian, Inc.," in line:
            self.skip_lines(inputfile, ["b", "s"])
            _, _, platform_full_version, compile_date = next(inputfile).split()
            run_date = next(inputfile).strip()
            platform_full_version_tokens = platform_full_version.split("-")
            full_version = platform_full_version_tokens[-1]
            platform = "-".join(platform_full_version_tokens[:-1])
            year_suffix = full_version[1:3]
            revision = full_version[6:]
            self.metadata[
                "package_version"
            ] = f"{self.YEAR_SUFFIXES_TO_YEARS[year_suffix]}+{revision}"
            self.metadata["platform"] = platform

        if line.strip().startswith("Link1:  Proceeding to internal job step number"):
            self.new_internal_job()

        # This block contains some general information as well as coordinates,
        # which could be parsed in the future:
        #
        # Symbolic Z-matrix:
        # Charge =  0 Multiplicity = 1
        # C                     0.73465   0.        0.
        # C                     1.93465   0.        0.
        # C
        # ...
        #
        # It also lists fragments, if there are any, which is potentially valuable:
        #
        # Symbolic Z-matrix:
        # Charge =  0 Multiplicity = 1 in supermolecule
        # Charge =  0 Multiplicity = 1 in fragment      1.
        # Charge =  0 Multiplicity = 1 in fragment      2.
        # B(Fragment=1)         0.06457  -0.0279    0.01364
        # H(Fragment=1)         0.03117  -0.02317   1.21604
        # ...
        #
        # Note, however, that currently we only parse information for the whole system
        # or supermolecule as Gaussian calls it.
        if line.strip() == "Symbolic Z-matrix:":

            self.updateprogress(inputfile, "Symbolic Z-matrix", self.fupdate)

            line = next(inputfile)
            while line.split()[0] == 'Charge':

                # For the supermolecule, we can parse the charge and multicplicity.
                regex = r".*=(.*)Mul.*=\s*-?(\d+).*"
                match = re.match(regex, line)
                assert match, f"Something unusual about the line: '{line}'"

                self.set_attribute('charge', int(match.groups()[0]))
                self.set_attribute('mult', int(match.groups()[1]))

                if line.split()[-2] == "fragment":
                    self.nfragments = int(line.split()[-1].strip('.'))

                if line.strip()[-13:] == "model system.":
                    self.nmodels = getattr(self, 'nmodels', 0) + 1

                line = next(inputfile)

            # The remaining part will allow us to get the atom count.
            # When coordinates are given, there is a blank line at the end, but if
            # there is a Z-matrix here, there will also be variables and we need to
            # stop at those to get the right atom count.
            # Also, in older versions there is bo blank line (G98 regressions),
            # so we need to watch out for leaving the link.
            natom = 0
            while line.split() and not "Variables" in line and not "Leave Link" in line:
                natom += 1
                line = next(inputfile)
            self.set_attribute('natom', natom)

        # Continuing from above, there is not always a symbolic matrix, for example
        # if the Z-matrix was in the input file. In such cases, try to match the
        # line and get at the charge and multiplicity.
        #
        #   Charge =  0 Multiplicity = 1 in supermolecule
        #   Charge =  0 Multiplicity = 1 in fragment  1.
        #   Charge =  0 Multiplicity = 1 in fragment  2.
        if line[1:7] == 'Charge' and line.find("Multiplicity") >= 0:

            self.updateprogress(inputfile, "Charge and Multiplicity", self.fupdate)

            if line.split()[-1] == "supermolecule" or (not "fragment" in line and not "model system" in line):

                regex = r".*=(.*)Mul.*=\s*-?(\d+).*"
                match = re.match(regex, line)
                assert match, f"Something unusual about the line: '{line}'"

                self.set_attribute('charge', int(match.groups()[0]))
                self.set_attribute('mult', int(match.groups()[1]))

            if line.split()[-2] == "fragment":
                self.nfragments = int(line.split()[-1].strip('.'))

            if line.strip()[-13:] == "model system.":
                self.nmodels = getattr(self, 'nmodels', 0) + 1

        # Number of atoms is also explicitely printed after the above.
        if line[1:8] == "NAtoms=":

            self.updateprogress(inputfile, "Attributes", self.fupdate)

            natom = int(re.search(r'NAtoms=\s*(\d+)', line).group(1))
            self.set_attribute('natom', natom)

            # Necessary for `if line.strip().split()[0:3] == ["Atom", "AN", "X"]:` block
            if not hasattr(self, 'nqmf'):
                match = re.search(r'NQMF=\s*(\d+)', line)
                if match is not None:
                    nqmf = int(match.group(1))
                    if nqmf > 0:
                        self.set_attribute('nqmf', nqmf)

        # Basis set name
        if line[1:15] == "Standard basis":
            self.metadata["basis_set"] = line.split()[2]
            
        # Solvent information.
        # PCM (the default gaussian solvent method).
        if line[1:34] == "Polarizable Continuum Model (PCM)":
            # Solvent parameters are unique to each solvent model,
            # so they are packaged together to prevent clogging the namespace.
            self.metadata['solvent_params'] = {
            }
            
            # Keep looking until dashed only.
            while set(line.strip()) != set("-"):
                line = next(inputfile)
                
                # PCM has a few different subtypes.
                # Model                : PCM.
                if "Model" in line:
                    self.metadata['solvent_model'] = " ".join(line.split()[2:])[:-1]
                    
                    if self.metadata['solvent_model'] == "PCM":
                        self.metadata['solvent_model'] = "IEFPCM"
                    
                    elif self.metadata['solvent_model'] == "C-PCM":
                        self.metadata['solvent_model'] = "CPCM"
                
                elif "Atomic radii" in line and line.split()[-1] == "SMD-Coulomb.":
                    self.metadata['solvent_model'] = "SMD-IEFPCM"
                    
                # Solvent by keyword.
                #  Solvent              : Toluene, Eps=   2.374100 Eps(inf)=   2.238315
                # Solvent by definition.
                #  Solvent              : Generic,
                #            Eps                           =   9.000000
                #            Eps(infinity)                 =   2.000000

                elif "Solvent" in line and "Eps=" in line and "Eps(inf)= " in line:
                    split_line = line.split()
                    
                    # Capture the human readable name, as well as params.
                    self.metadata['solvent_name'] = split_line[2][:-1].lower()
                    
                    self.metadata['solvent_params']['epsilon'] = float(split_line[4])
                    self.metadata['solvent_params']['epsilon_infinite'] = float(split_line[6])
                
                elif "Eps(infinity)" in line:
                    # Assume manually specified solvent.
                    self.metadata['solvent_params']['epsilon_infinite'] = float(line.split()[-1])
                    
                elif "Eps" in line:
                    # Assume manually specified solvent.
                    self.metadata['solvent_params']['epsilon'] = float(line.split()[-1])
        
        elif "Reaction Field using a Density IsoSurface Boundary" in line:
            self.metadata['solvent_model'] = "IPCM"
        
        #  Epsi=   78.3000 Cont =    0.0010
        elif  "Epsi=" in line and "Cont =":
            if "solvent_params" not in self.metadata:
                self.metadata['solvent_params'] = {}
            
            self.metadata['solvent_params']['epsilon'] = float(line.split()[1])
            self.metadata['solvent_params']['isovalue'] = float(line.split()[4])

        # Dipole moment
        # e.g. from G09
        #  Dipole moment (field-independent basis, Debye):
        #    X=              0.0000    Y=              0.0000    Z=              0.0930
        # e.g. from G03
        #     X=     0.0000    Y=     0.0000    Z=    -1.6735  Tot=     1.6735
        # need the "field independent" part - ONIOM and other calc use diff formats
        if line[1:39] == "Dipole moment (field-independent basis":

            self.updateprogress(inputfile, "Dipole and Higher Moments", self.fupdate)

            self.reference = [0.0, 0.0, 0.0]
            self.moments = [self.reference]

            tokens = next(inputfile).split()
            # split - dipole would need to be *huge* to fail a split
            # and G03 and G09 use different spacing
            if len(tokens) >= 6:
                dipole = (float(tokens[1]), float(tokens[3]), float(tokens[5]))

            if not hasattr(self, 'moments'):
                self.moments = [self.reference, dipole]
            else:
                self.moments.append(dipole)

        if line[1:43] == "Quadrupole moment (field-independent basis":
            # e.g. (g09)
            # Quadrupole moment (field-independent basis, Debye-Ang):
            #   XX=             -6.1213   YY=             -4.2950   ZZ=             -5.4175
            #   XY=              0.0000   XZ=              0.0000   YZ=              0.0000
            # or from g03
            #   XX=    -6.1213   YY=    -4.2950   ZZ=    -5.4175
            quadrupole = {}
            for j in range(2): # two rows
                line = next(inputfile)
                if line[22] == '=': # g03 file
                    for i in (1, 18, 35):
                        quadrupole[line[i:i+4]] = float(line[i+5:i+16])
                else:
                    for i in (1, 27, 53):
                        quadrupole[line[i:i+4]] = float(line[i+5:i+25])

            lex = sorted(quadrupole.keys())
            quadrupole = [quadrupole[key] for key in lex]

            if not hasattr(self, 'moments') or len(self.moments) < 2:
                self.logger.warning("Found quadrupole moments but no previous dipole")
                self.reference = [0.0, 0.0, 0.0]
                self.moments = [self.reference, None, quadrupole]
            else:
                if len(self.moments) == 2:
                    self.moments.append(quadrupole)
                else:
                    assert self.moments[2] == quadrupole

        if line[1:41] == "Octapole moment (field-independent basis":
            # e.g.
            # Octapole moment (field-independent basis, Debye-Ang**2):
            #  XXX=              0.0000  YYY=              0.0000  ZZZ=             -0.1457  XYY=              0.0000
            #  XXY=              0.0000  XXZ=              0.0136  XZZ=              0.0000  YZZ=              0.0000
            #  YYZ=             -0.5848  XYZ=              0.0000
            octapole = {}
            for j in range(2): # two rows
                line = next(inputfile)
                if line[22] == '=': # g03 file
                    for i in (1, 18, 35, 52):
                        octapole[line[i:i+4]] = float(line[i+5:i+16])
                else:
                    for i in (1, 27, 53, 79):
                        octapole[line[i:i+4]] = float(line[i+5:i+25])

            # last line only 2 moments
            line = next(inputfile)
            if line[22] == '=': # g03 file
                for i in (1, 18):
                    octapole[line[i:i+4]] = float(line[i+5:i+16])
            else:
                for i in (1, 27):
                    octapole[line[i:i+4]] = float(line[i+5:i+25])

            lex = sorted(octapole.keys())
            octapole = [octapole[key] for key in lex]

            if not hasattr(self, 'moments') or len(self.moments) < 3:
                self.logger.warning("Found octapole moments but no previous dipole or quadrupole")
                self.reference = [0.0, 0.0, 0.0]
                self.moments = [self.reference, None, None, octapole]
            else:
                if len(self.moments) == 3:
                    self.moments.append(octapole)
                else:
                    assert self.moments[3] == octapole

        if line[1:20] == "Hexadecapole moment":
            # e.g.
            # Hexadecapole moment (field-independent basis, Debye-Ang**3):
            # XXXX=             -3.2614 YYYY=             -6.8264 ZZZZ=             -4.9965 XXXY=              0.0000
            # XXXZ=              0.0000 YYYX=              0.0000 YYYZ=              0.0000 ZZZX=              0.0000
            # ZZZY=              0.0000 XXYY=             -1.8585 XXZZ=             -1.4123 YYZZ=             -1.7504
            # XXYZ=              0.0000 YYXZ=              0.0000 ZZXY=              0.0000
            hexadecapole = {}
            # read three lines worth of 4 moments per line
            for j in range(3):
                line = next(inputfile)
                if line[22] == '=': # g03 file
                    for i in (1, 18, 35, 52):
                        hexadecapole[line[i:i+4]] = float(line[i+5:i+16])
                else:
                    for i in (1, 27, 53, 79):
                        hexadecapole[line[i:i+4]] = float(line[i+5:i+25])

            # last line only 3 moments
            line = next(inputfile)
            if line[22] == '=': # g03 file
                for i in (1, 18, 35):
                    hexadecapole[line[i:i+4]] = float(line[i+5:i+16])
            else:
                for i in (1, 27, 53):
                    hexadecapole[line[i:i+4]] = float(line[i+5:i+25])

            lex = sorted(hexadecapole.keys())
            hexadecapole = [hexadecapole[key] for key in lex]

            if not hasattr(self, 'moments') or len(self.moments) < 4:
                self.reference = [0.0, 0.0, 0.0]
                self.moments = [self.reference, None, None, None, hexadecapole]
            else:
                if len(self.moments) == 4:
                    self.append_attribute("moments", hexadecapole)
                else:
                    try:
                        numpy.testing.assert_equal(self.moments[4], hexadecapole)
                    except AssertionError:
                        self.logger.warning(
                            f"Attribute hexadecapole changed value ({self.moments[4]} -> {hexadecapole})"
                        )
                    self.append_attribute("moments", hexadecapole)

        # Catch message about completed optimization.
        if line[1:23] == "Optimization completed":

            if not hasattr(self, 'optdone'):
                self.optdone = []
            self.optdone.append(len(self.geovalues) - 1)

            assert hasattr(self, "optstatus") and len(self.optstatus) > 0
            self.optstatus[-1] += data.ccData.OPT_DONE

        # Catch message about stopped optimization (not converged).
        if line[1:21] == "Optimization stopped":

            if not hasattr(self, "optdone"):
                self.optdone = []

            assert hasattr(self, "optstatus") and len(self.optstatus) > 0
            self.optstatus[-1] += data.ccData.OPT_UNCONVERGED

        # Extract the atomic numbers and coordinates from the input orientation,
        #   in the event the standard orientation isn't available.
        # Don't extract from Input or Z-matrix orientation in a BOMD run, as only
        #   the final geometry should be kept but extract inputatoms.
        # We also use "inputcoords" to convert "grads" from input orientation
        #   to standard orientation
        if line.find("Input orientation") > -1 or line.find("Z-Matrix orientation") > -1:

            # If this is a counterpoise calculation, this output means that
            #   the supermolecule is now being considered, so we can set:
            self.counterpoise = 0

            self.updateprogress(inputfile, "Attributes", self.cupdate)

            if not self.BOMD and  not hasattr(self, "inputcoords"):
                self.inputcoords = []
            self.inputatoms = []

            self.skip_lines(inputfile, ['d', 'cols', 'cols', 'd'])

            atomcoords = []
            line = next(inputfile)
            while list(set(line.strip())) != ["-"]:
                broken = line.split()
                atomno = int(broken[1])
                # Atom with atomno -1 only appears on "Z-Matrix orientation", and excluded on
                #   "Input orientation" or "Standard orientation".
                # We remove this line to keep shape consistency of "atomcoords" and "inputcoords",
                #   so that we can convert "grads" from input orientation to standard orientaion
                #   with rotation matrix calculated from "atomcoords" and "inputcoords"
                if atomno != -1:
                    self.inputatoms.append(atomno)
                    atomcoords.append(list(map(float, broken[3:6])))
                line = next(inputfile)

            if not self.BOMD: self.inputcoords.append(atomcoords)

            self.set_attribute('atomnos', numpy.array(self.inputatoms))
            self.set_attribute('natom', len(self.inputatoms))

        if self.BOMD and line.startswith(' Summary information for step'):

            # We keep time and energies_BOMD and coordinates in a dictionary
            #  because steps can be recalculated, and we need to overwite the
            #  previous data

            broken = line.split()
            step = int(broken[-1])
            line = next(inputfile)
            broken = line.split()
            if not hasattr(self, "time"):
                self.set_attribute('time', {step:float(broken[-1])})
            else:
                self.time[step] = float(broken[-1])

            line = next(inputfile)
            broken = line.split(';')[1].split()
            ene = utils.convertor(utils.float(broken[-1]), "hartree", "eV")
            if not hasattr(self, "energies_BOMD"):
                self.set_attribute('energies_BOMD', {step:ene})
            else:
                self.energies_BOMD[step] = ene

            self.updateprogress(inputfile, "Attributes", self.cupdate)

            if not hasattr(self, "atomcoords_BOMD"):
                self.atomcoords_BOMD = {}
            #self.inputatoms = []

            self.skip_lines(inputfile, ['EKin', 'Angular', 'JX', 'Total', 'Total', 'Cartesian'])

            atomcoords = []
            line = next(inputfile)
            while not "MW cartesian" in line:
                broken = line.split()
                atomcoords.append(list(map(utils.float, (broken[3], broken[5], broken[7]))))
            #    self.inputatoms.append(int(broken[1]))
                line = next(inputfile)

            self.atomcoords_BOMD[step] = atomcoords

            #self.set_attribute('atomnos', self.inputatoms)
            #self.set_attribute('natom', len(self.inputatoms))


        # Extract the atomic masses.
        # Typical section:
        #                    Isotopes and Nuclear Properties:
        #(Nuclear quadrupole moments (NQMom) in fm**2, nuclear magnetic moments (NMagM)
        # in nuclear magnetons)
        #
        #  Atom         1           2           3           4           5           6           7           8           9          10
        # IAtWgt=          12          12          12          12          12           1           1           1          12          12
        # AtmWgt=  12.0000000  12.0000000  12.0000000  12.0000000  12.0000000   1.0078250   1.0078250   1.0078250  12.0000000  12.0000000
        # NucSpn=           0           0           0           0           0           1           1           1           0           0
        # AtZEff=  -3.6000000  -3.6000000  -3.6000000  -3.6000000  -3.6000000  -1.0000000  -1.0000000  -1.0000000  -3.6000000  -3.6000000
        # NQMom=    0.0000000   0.0000000   0.0000000   0.0000000   0.0000000   0.0000000   0.0000000   0.0000000   0.0000000   0.0000000
        # NMagM=    0.0000000   0.0000000   0.0000000   0.0000000   0.0000000   2.7928460   2.7928460   2.7928460   0.0000000   0.0000000
        # ... with blank lines dividing blocks of ten, and Leave Link 101 at the end.
        # This is generally parsed before coordinates, so atomnos is not defined.
        # Note that in Gaussian03 the comments are not there yet and the labels are different.
        if line.strip() == "Isotopes and Nuclear Properties:":

            if not hasattr(self, "atommasses"):
                self.atommasses = []

            line = next(inputfile)
            while line[1:16] != "Leave Link  101":
                if line[1:8] == "AtmWgt=":
                    self.atommasses.extend(list(map(float, line.split()[1:])))
                line = next(inputfile)

        # Symmetry: point group
        if "Symmetry turned off" in line:
            self.set_attribute('uses_symmetry', False)
        if "Full point group" in line:
            point_group_detected = line.split()[3].lower()
            if self.uses_symmetry:
                while "Largest Abelian subgroup" not in line:
                    line = next(inputfile)
                    if "Leave Link" in line:
                        # TODO To handle this properly, it needs to be
                        # calculated from the full point group.
                        point_group_used = point_group_detected
                        break
                if "Leave Link" not in line:
                    point_group_used = line.split()[3].lower()
            else:
                point_group_used = "c1"
            self.metadata['symmetry_detected'] = point_group_detected
            self.metadata['symmetry_used'] = point_group_used

        # Symmetry: ordering of irreducible representations
        if "symmetry adapted cartesian basis functions" in line:
            if not hasattr(self, 'symlabels'):
                self.symlabels = []
            irrep = self.normalisesym(line.split()[-2])
            self.symlabels.append(irrep)

        # Extract the atomic numbers and coordinates of the atoms.
        if line.strip() == "Standard orientation:":

            self.updateprogress(inputfile, "Attributes", self.cupdate)

            # If this is a counterpoise calculation, this output means that
            #   the supermolecule is now being considered, so we can set:
            self.counterpoise = 0

            if not hasattr(self, "atomcoords"):
                self.atomcoords = []

            self.skip_lines(inputfile, ['d', 'cols', 'cols', 'd'])

            atomnos = []
            atomcoords = []
            line = next(inputfile)
            while list(set(line.strip())) != ["-"]:
                broken = line.split()
                atomnos.append(int(broken[1]))
                atomcoords.append(list(map(float, broken[-3:])))
                line = next(inputfile)
            self.atomcoords.append(atomcoords)

            self.set_attribute('natom', len(atomnos))
            self.set_attribute('atomnos', atomnos)

        # This is a bit of a hack for regression Gaussian09/BH3_fragment_guess.pop_minimal.log
        # to skip output for all fragments, assuming the supermolecule is always printed first.
        # Eventually we want to make this more general, or even better parse the output for
        # all fragment, but that will happen in a newer version of cclib.
        if line[1:16] == "Fragment guess:" and getattr(self, 'nfragments', 0) > 1:
            if not "full" in line:
                raise StopParsing()

        # Another hack for regression Gaussian03/ortho_prod_freq.log, which is an ONIOM job.
        # Basically for now we stop parsing after the output for the real system, because
        # currently we don't support changes in system size or fragments in cclib. When we do,
        # we will want to parse the model systems, too, and that is what nmodels could track.
        if "ONIOM: generating point" in line and line.strip()[-13:] == 'model system.' and getattr(self, 'nmodels', 0) > 0:
            while not line[1:30] == 'ONIOM: Integrating ONIOM file':
                line = next(inputfile)

        # With the gfinput keyword, the atomic basis set functions are:
        #
        # AO basis set in the form of general basis input (Overlap normalization):
        #  1 0
        # S   3 1.00       0.000000000000
        #      0.7161683735D+02  0.1543289673D+00
        #      0.1304509632D+02  0.5353281423D+00
        #      0.3530512160D+01  0.4446345422D+00
        # SP   3 1.00       0.000000000000
        #      0.2941249355D+01 -0.9996722919D-01  0.1559162750D+00
        #      0.6834830964D+00  0.3995128261D+00  0.6076837186D+00
        #      0.2222899159D+00  0.7001154689D+00  0.3919573931D+00
        # ****
        #  2 0
        # S   3 1.00       0.000000000000
        #      0.7161683735D+02  0.1543289673D+00
        # ...
        #
        # The same is also printed when the gfprint keyword is used, but the
        # interstitial lines differ and there are no stars between atoms:
        #
        # AO basis set (Overlap normalization):
        # Atom C1       Shell     1 S   3     bf    1 -     1          0.509245180608         -2.664678875191          0.000000000000
        #       0.7161683735D+02  0.1543289673D+00
        #       0.1304509632D+02  0.5353281423D+00
        #       0.3530512160D+01  0.4446345422D+00
        # Atom C1       Shell     2 SP   3    bf    2 -     5          0.509245180608         -2.664678875191          0.000000000000
        #       0.2941249355D+01 -0.9996722919D-01  0.1559162750D+00
        # ...

        #ONIOM calculations result basis sets reported for atoms that are not in order of atom number which breaks this code (line 390 relies on atoms coming in order)
        if line[1:13] == "AO basis set" and not self.oniom:

            self.gbasis = []

            # For counterpoise fragment calcualtions, skip these lines.
            if self.counterpoise != 0:
                return

            atom_line = next(inputfile)
            self.gfprint = atom_line.split()[0] == "Atom"
            self.gfinput = not self.gfprint

            # Note how the shell information is on a separate line for gfinput,
            # whereas for gfprint it is on the same line as atom information.
            if self.gfinput:
                shell_line = next(inputfile)

            shell = []
            while len(self.gbasis) < self.natom:

                if self.gfprint:
                    cols = atom_line.split()
                    subshells = cols[4]
                    ngauss = int(cols[5])
                else:
                    cols = shell_line.split()
                    subshells = cols[0]
                    ngauss = int(cols[1])

                parameters = []
                for ig in range(ngauss):
                    line = next(inputfile)
                    parameters.append(list(map(utils.float, line.split())))
                for iss, ss in enumerate(subshells):
                    contractions = []
                    for param in parameters:
                        exponent = param[0]
                        coefficient = param[iss+1]
                        contractions.append((exponent, coefficient))
                    subshell = (ss, contractions)
                    shell.append(subshell)

                if self.gfprint:
                    line = next(inputfile)
                    if line.split()[0] == "Atom":
                        atomnum = int(re.sub(r"\D", "", line.split()[1]))
                        if atomnum == len(self.gbasis) + 2:
                            self.gbasis.append(shell)
                            shell = []
                        atom_line = line
                    else:
                        self.gbasis.append(shell)
                else:
                    line = next(inputfile)
                    if line.strip() == "****":
                        self.gbasis.append(shell)
                        shell = []
                        atom_line = next(inputfile)
                        shell_line = next(inputfile)
                    else:
                        shell_line = line

        if "Dispersion energy=" in line:
            dispersion = utils.convertor(float(line.split()[-2]), "hartree", "eV")
            self.append_attribute("dispersionenergies", dispersion)

        # Find the targets for SCF convergence (QM calcs).
        # Not for BOMD as targets are not available in the summary
        if not self.BOMD and line[1:44] == 'Requested convergence on RMS density matrix':

            if not hasattr(self, "scftargets"):
                self.scftargets = []
            # The following can happen with ONIOM which are mixed SCF
            # and semi-empirical
            if type(self.scftargets) == type(numpy.array([])):
                self.scftargets = []

            scftargets = []
            # The RMS density matrix.
            scftargets.append(utils.float(line.split('=')[1].split()[0]))
            line = next(inputfile)
            # The MAX density matrix.
            scftargets.append(utils.float(line.strip().split('=')[1][:-1]))
            line = next(inputfile)
            # For G03, there's also the energy (not for G98).
            if line[1:10] == "Requested":
                scftargets.append(utils.float(line.strip().split('=')[1][:-1]))

            self.scftargets.append(scftargets)

        # Extract SCF convergence information (QM calcs).
        if line[1:10] == 'Cycle   1':

            if not hasattr(self, "scfvalues"):
                self.scfvalues = []

            scfvalues = []
            line = next(inputfile)
            while line.find("SCF Done") == -1:

                self.updateprogress(inputfile, "QM convergence", self.fupdate)
                
                # SCI-PCM solvent info appears in each SCF section...
                #  Compute SCI-PCM surface.
                if "Compute SCI-PCM surface" in line:
                    self.metadata['solvent_model'] = "SCIPCM"
                
                # For SCI-PCM.
                # Dielectric constant of solvent =     2.374100"
                if line[1:33] == "Dielectric constant of solvent =":
                    if "solvent_params" not in self.metadata:
                        self.metadata['solvent_params'] = {}
                    
                    self.metadata["solvent_params"]['epsilon'] = float(line.split()[-1])
                
                if line.find(' E=') == 0:
                    self.logger.debug(line)

                #  RMSDP=3.74D-06 MaxDP=7.27D-05 DE=-1.73D-07 OVMax= 3.67D-05
                # or
                #  RMSDP=1.13D-05 MaxDP=1.08D-04              OVMax= 1.66D-04
                if line.find(" RMSDP") == 0:

                    # Fields of interest:
                    # RMSDP
                    # MaxDP
                    # (DE) -> Only add the energy if it's a target criteria

                    matches = self.re_scinot.findall(line)
                    matches = {
                        match[0]: utils.float(match[1])
                        for match in matches
                    }
                    scfvalues_step = [
                        matches.get('RMSDP', numpy.nan),
                        matches.get('MaxDP', numpy.nan)
                    ]
                    if hasattr(self, "scftargets") and len(self.scftargets[0]) == 3:
                        scfvalues_step.append(matches.get('DE', numpy.nan))
                    scfvalues.append(scfvalues_step)

                try:
                    line = next(inputfile)
                # May be interupted by EOF.
                except StopIteration:
                    self.logger.warning('File terminated before end of last SCF!')
                    break

            self.scfvalues.append(scfvalues)

        # Extract SCF convergence information (AM1, INDO and other semi-empirical calcs).
        # The output (for AM1) looks like this:
        # Ext34=T Pulay=F Camp-King=F BShift= 0.00D+00
        # It=  1 PL= 0.103D+01 DiagD=T ESCF=     31.564733 Diff= 0.272D+02 RMSDP= 0.152D+00.
        # It=  2 PL= 0.114D+00 DiagD=T ESCF=      7.265370 Diff=-0.243D+02 RMSDP= 0.589D-02.
        # ...
        # It= 11 PL= 0.184D-04 DiagD=F ESCF=      4.687669 Diff= 0.260D-05 RMSDP= 0.134D-05.
        # It= 12 PL= 0.105D-04 DiagD=F ESCF=      4.687669 Diff=-0.686D-07 RMSDP= 0.215D-05.
        # 4-point extrapolation.
        # It= 13 PL= 0.110D-05 DiagD=F ESCF=      4.687669 Diff=-0.111D-06 RMSDP= 0.653D-07.
        # Energy=    0.172272018655 NIter=  14.
        if line[1:4] == 'It=':

            scftargets = numpy.array([1E-7], "d")  # This is the target value for the rms
            scfvalues = [[]]

            while line.find(" Energy") == -1:

                self.updateprogress(inputfile, "AM1 Convergence")

                if line[1:4] == "It=":
                    parts = line.strip().split()
                    scfvalues[0].append(utils.float(parts[-1][:-1]))

                line = next(inputfile)

                # If an AM1 or INDO guess is used (Guess=INDO in the input, for example),
                # this will be printed after a single iteration, so that is the line
                # that should trigger a break from this loop. At least that's what we see
                # for regression Gaussian/Gaussian09/guessIndo_modified_ALT.out
                if line[:14] == " Initial guess":
                    break

            # Attach the attributes to the object Only after the energy is found .
            if line.find(" Energy") == 0:
                self.scftargets = scftargets
                self.scfvalues = scfvalues

        # Note: this needs to follow the section where 'SCF Done' is used
        #   to terminate a loop when extracting SCF convergence information.
        if not self.BOMD and line[1:9] == 'SCF Done':

            t1 = line.split()[2]
            if t1 == 'E(RHF)':
                self.metadata["methods"].append("HF")
            else:
                self.metadata["methods"].append("DFT")
                self.metadata["functional"] = t1[t1.index("(") + 2:t1.rindex(")")]

            if not hasattr(self, "scfenergies"):
                self.scfenergies = []

            self.scfenergies.append(utils.convertor(utils.float(line.split()[4]), "hartree", "eV"))
        # gmagoon 5/27/09: added scfenergies reading for PM3 case
        # Example line: " Energy=   -0.077520562724 NIter=  14."
        # See regression Gaussian03/QVGXLLKOCUKJST-UHFFFAOYAJmult3Fixed.out
        if line[1:8] == 'Energy=':
            if not hasattr(self, "scfenergies"):
                self.scfenergies = []
            self.scfenergies.append(utils.convertor(utils.float(line.split()[1]), "hartree", "eV"))

        # Total energies after Moller-Plesset corrections.
        # Second order correction is always first, so its first occurance
        #   triggers creation of mpenergies (list of lists of energies).
        # Further MP2 corrections are appended as found.
        #
        # Example MP2 output line:
        #  E2 =    -0.9505918144D+00 EUMP2 =    -0.28670924198852D+03
        # Warning! this output line is subtly different for MP3/4/5 runs
        # Newer versions of gausian introduced a space between 'EUMP2' and '='...
        if "EUMP2 =" in line[27:36] or "EUMP2=" in line[27:35]:
            self.metadata["methods"].append("MP2")

            if not hasattr(self, "mpenergies"):
                self.mpenergies = []
            self.mpenergies.append([])
            mp2energy = utils.float(line.split("=")[2])
            self.mpenergies[-1].append(utils.convertor(mp2energy, "hartree", "eV"))

        # Example MP3 output line:
        #  E3=       -0.10518801D-01     EUMP3=      -0.75012800924D+02
        if line[34:40] == "EUMP3=":
            self.metadata["methods"].append("MP3")

            mp3energy = utils.float(line.split("=")[2])
            self.mpenergies[-1].append(utils.convertor(mp3energy, "hartree", "eV"))

        # Example MP4 output lines:
        #  E4(DQ)=   -0.31002157D-02        UMP4(DQ)=   -0.75015901139D+02
        #  E4(SDQ)=  -0.32127241D-02        UMP4(SDQ)=  -0.75016013648D+02
        #  E4(SDTQ)= -0.32671209D-02        UMP4(SDTQ)= -0.75016068045D+02
        # Energy for most substitutions is used only (SDTQ by default)
        if line[34:43] == "UMP4(DQ)=":
            self.metadata["methods"].append("MP4")

            mp4energy = utils.float(line.split("=")[2])
            line = next(inputfile)
            if line[34:44] == "UMP4(SDQ)=":
                mp4energy = utils.float(line.split("=")[2])
                line = next(inputfile)
                if line[34:45] == "UMP4(SDTQ)=":
                    mp4energy = utils.float(line.split("=")[2])
            self.mpenergies[-1].append(utils.convertor(mp4energy, "hartree", "eV"))

        # Example MP5 output line:
        #  DEMP5 =  -0.11048812312D-02 MP5 =  -0.75017172926D+02
        if line[29:34] == "MP5 =":
            self.metadata["methods"].append("MP5")
            mp5energy = utils.float(line.split("=")[2])
            self.mpenergies[-1].append(utils.convertor(mp5energy, "hartree", "eV"))

        # Total energies after Coupled Cluster corrections.
        # Second order MBPT energies (MP2) are also calculated for these runs,
        # but the output is the same as when parsing for mpenergies.
        # Read the consecutive correlated energies
        # but append only the last one to ccenergies.
        # Only the highest level energy is appended - ex. CCSD(T), not CCSD.
        if line[1:10] == "DE(Corr)=" and line[27:35] == "E(CORR)=":
            self.metadata["methods"].append("CCSD")
            self.ccenergy = utils.float(line.split()[3])
        if line[1:14] == "T1 Diagnostic":
            self.metadata["t1_diagnostic"] = utils.float(line.split()[-1])
        if line[1:10] == "T5(CCSD)=":
            line = next(inputfile)
            if line[1:9] == "CCSD(T)=":
                self.metadata["methods"].append("CCSD-T")
                self.ccenergy = utils.float(line.split()[1])
        # Find step number for current optimization/IRC
        # Matches "Step number  123", "Pt XX Step number 123" and "PtXXX Step number 123"
        if " Step number" in line:
            step = int(line.split()[line.split().index('Step') + 2])
            if not hasattr(self, "optstatus"):
                self.optstatus = []
            self.optstatus.append(data.ccData.OPT_UNKNOWN)
            if step == 1:
                self.optstatus[-1] += data.ccData.OPT_NEW

        # Geometry convergence information.
        if line[49:59] == 'Converged?':

            if not hasattr(self, "geotargets"):
                self.geovalues = []
                self.geotargets = numpy.array([0.0, 0.0, 0.0, 0.0], "d")
            allconverged = True
            newlist = [0]*4
            for i in range(4):
                line = next(inputfile)
                self.logger.debug(line)
                parts = line.split()
                if "NO" in parts[-1]:
                    allconverged = False
                try:
                    value = utils.float(parts[2])
                except ValueError:
                    self.logger.error(
                        f"Problem parsing the value for geometry optimisation: {parts[2]} is not a number."
                    )
                else:
                    newlist[i] = value
                self.geotargets[i] = utils.float(parts[3])

            self.geovalues.append(newlist)

        # Gradients.
        # Read in the cartesian energy gradients (forces) from a block like this:
        # -------------------------------------------------------------------
        # Center     Atomic                   Forces (Hartrees/Bohr)
        # Number     Number              X              Y              Z
        # -------------------------------------------------------------------
        # 1          1          -0.012534744   -0.021754635   -0.008346094
        # 2          6           0.018984731    0.032948887   -0.038003451
        # 3          1          -0.002133484   -0.006226040    0.023174772
        # 4          1          -0.004316502   -0.004968213    0.023174772
        #           -2          -0.001830728   -0.000743108   -0.000196625
        # ------------------------------------------------------------------
        #
        # The "-2" line is for a dummy atom
        #
        # Then optimization is done in internal coordinates, Gaussian also
        # print the forces in internal coordinates, which can be produced from
        # the above. This block looks like this:
        # Variable       Old X    -DE/DX   Delta X   Delta X   Delta X     New X
        #                                 (Linear)    (Quad)   (Total)
        #   ch        2.05980   0.01260   0.00000   0.01134   0.01134   2.07114
        #   hch        1.75406   0.09547   0.00000   0.24861   0.24861   2.00267
        #   hchh       2.09614   0.01261   0.00000   0.16875   0.16875   2.26489
        #         Item               Value     Threshold  Converged?

        # We could get the gradients in BOMD, but it is more complex because
        #   they are not in the summary, and they are not as relevant as for
        #   an optimization
        if not self.BOMD and line[37:43] == "Forces":

            if not hasattr(self, "grads"):
                self.grads = []

            self.skip_lines(inputfile, ['header', 'd'])

            forces = []
            line = next(inputfile)
            while list(set(line.strip())) != ['-']:
                tmpforces = []
                for N in range(3):  # Fx, Fy, Fz
                    force = line[23+N*15:38+N*15]
                    if force.startswith("*"):
                        force = "NaN"
                    tmpforces.append(float(force))
                forces.append(tmpforces)
                line = next(inputfile)
            self.grads.append(forces)

        if "Number of optimizations in scan" in line:
            self.scan_length = int(line.split()[-1])

        # All PES scans have a list of initial parameters from which we
        # can get the names and more.
        #
        #                            ----------------------------
        #                           !    Initial Parameters    !
        #                           ! (Angstroms and Degrees)  !
        # --------------------------                            --------------------------
        # ! Name  Definition              Value          Derivative Info.                !
        # --------------------------------------------------------------------------------
        # ! R1    R(1,2)                  1.4212         estimate D2E/DX2                !
        # ! R2    R(1,14)                 1.4976         estimate D2E/DX2                !
        # ...
        if "Initial Parameters" in line:
            self.scannames_all = []
            self.scannames_scanned = []
            self.skip_lines(inputfile, ['units', 'd', 'header', 'd'])
            line = next(inputfile)
            while line.strip()[0] == '!':
                name = line.split()[1]
                definition = line.split()[2]
                self.scannames_all.append(name)
                if line.split()[4] == 'Scan':
                    self.scannames_scanned.append(name)
                    self.append_attribute('scannames', definition)
                line = next(inputfile)

        # Extract unrelaxed PES scan data, which looks something like:
        #
        # Summary of the potential surface scan:
        #   N       A          SCF
        # ----  ---------  -----------
        #    1   109.0000    -76.43373
        #    2   119.0000    -76.43011
        #    3   129.0000    -76.42311
        #    4   139.0000    -76.41398
        #    5   149.0000    -76.40420
        #    6   159.0000    -76.39541
        #    7   169.0000    -76.38916
        #    8   179.0000    -76.38664
        #    9   189.0000    -76.38833
        #   10   199.0000    -76.39391
        #   11   209.0000    -76.40231
        # ----  ---------  -----------
        if "Summary of the potential surface scan:" in line:

            colmnames = next(inputfile)
            if not hasattr(self, "scannames"):
                self.set_attribute("scannames", colmnames.split()[1:-1])

            hyphens = next(inputfile)
            line = next(inputfile)
            scanparm = [[] for _ in range(len(self.scannames))]
            while line != hyphens:
                broken = line.split()
                self.append_attribute('scanenergies', (utils.convertor(float(broken[-1]), "hartree", "eV")))
                for idx,p in enumerate(broken[1:-1]):
                    scanparm[idx].append(float(p))
                #self.append_attribute('scanparm', [float(p) for p in broken[1:-1]])
                line = next(inputfile)
            self.set_attribute('scanparm', scanparm)

        # Extract relaxed (optimized) PES scan data, for which the form
        # of the output is transposed:
        #
        #  Summary of Optimized Potential Surface Scan (add -382.0 to energies):
        #                            1         2         3         4         5
        #      Eigenvalues --    -0.30827  -0.30695  -0.30265  -0.29955  -0.30260
        #            R1           1.42115   1.42152   1.42162   1.42070   1.42071
        #            R2           1.49761   1.49787   1.49855   1.49901   1.49858
        #            R3           1.42245   1.42185   1.42062   1.42048   1.42147
        #            R4           1.40217   1.40236   1.40306   1.40441   1.40412
        # ...
        if "Summary of Optimized Potential Surface Scan" in line:
            # The base energy separation is version dependent, and first
            # appears in Gaussian16.
            base_energy = 0.0
            if "add" in line and "to energies" in line:
                base_energy = float(line.split()[-3])

            scanenergies = []
            scanparm = [[] for _ in range(len(self.scannames))]
            while len(scanenergies) != self.scan_length:
                line = next(inputfile)
                indices = [int(i) for i in line.split()]           
                widths = [10]*len(indices)
                splitter = utils.WidthSplitter(widths)

                line = next(inputfile)
                eigenvalues_in_line = line[21:].rstrip()
                assert len(eigenvalues_in_line) == sum(widths)
                cols = list(splitter.split(eigenvalues_in_line))
                try:
                    eigenvalues = [float(e) for e in cols]
                    eigenvalues = [base_energy + e for e in eigenvalues]
                except ValueError:
                    eigenvalues = [numpy.nan for _ in cols]
                assert len(eigenvalues) == len(indices)
                eigenvalues = [utils.convertor(e, "hartree", "eV") for e in eigenvalues]
                scanenergies.extend(eigenvalues)

                for _, name in enumerate(self.scannames_all):
                    line = next(inputfile)
                    assert line.split()[0] == name
                    if name in self.scannames_scanned:
                        iname = self.scannames_scanned.index(name)
                        params_in_line = line[21:].rstrip()
                        assert len(params_in_line) == sum(widths)
                        params = [float(v) for v in splitter.split(params_in_line)]
                        scanparm[iname].extend(params)

            self.set_attribute('scanenergies', scanenergies)
            self.set_attribute('scanparm', scanparm)

        # Orbital symmetries.
        if line[1:20] == 'Orbital symmetries:' and not hasattr(self, "mosyms"):

            # For counterpoise fragments, skip these lines.
            if self.counterpoise != 0:
                return

            self.updateprogress(inputfile, "MO Symmetries", self.fupdate)

            self.mosyms = [[]]
            line = next(inputfile)
            unres = False
            if line.find("Alpha Orbitals") == 1:
                unres = True
                line = next(inputfile)
            i = 0
            while len(line) > 18 and line[17] == '(':
                if line.find('Virtual') >= 0:
                    self.homos = [i - 1]
                parts = line[17:].split()
                for x in parts:
                    self.mosyms[0].append(self.normalisesym(x.strip('()')))
                    i += 1
                line = next(inputfile)
            if unres:
                line = next(inputfile)
                # Repeat with beta orbital information
                i = 0
                self.mosyms.append([])
                while len(line) > 18 and line[17] == '(':
                    if line.find('Virtual') >= 0:
                        # Here we consider beta
                        # If there was also an alpha virtual orbital,
                        #  we will store two indices in the array
                        # Otherwise there is no alpha virtual orbital,
                        #  only beta virtual orbitals, and we initialize
                        #  the array with one element. See the regression
                        #  QVGXLLKOCUKJST-UHFFFAOYAJmult3Fixed.out
                        #  donated by Gregory Magoon (gmagoon).
                        if (hasattr(self, "homos")):
                            # Extend the array to two elements
                            # 'HOMO' indexes the HOMO in the arrays
                            self.homos.append(i-1)
                        else:
                            # 'HOMO' indexes the HOMO in the arrays
                            self.homos = [i - 1]
                    parts = line[17:].split()
                    for x in parts:
                        self.mosyms[1].append(self.normalisesym(x.strip('()')))
                        i += 1
                    line = next(inputfile)

            # Some calculations won't explicitely print the number of basis sets used,
            # and will occasionally drop some without warning. We can infer the number,
            # however, from the MO symmetries printed here. Specifically, this fixes
            # regression Gaussian/Gaussian09/dvb_sp_terse.log (#23 on github).
            self.set_attribute('nmo', len(self.mosyms[-1]))

        # Alpha/Beta electron eigenvalues.
        if line[1:6] == "Alpha" and line.find("eigenvalues") >= 0:

            # For counterpoise fragments, skip these lines.
            if self.counterpoise != 0:
                return

            # For ONIOM calcs, ignore this section in order to bypass assertion failure.
            if self.oniom:
                return

            self.updateprogress(inputfile, "Eigenvalues", self.fupdate)
            self.moenergies = [[]]
            HOMO = -2

            while line.find('Alpha') == 1:
                if line.split()[1] == "virt." and HOMO == -2:

                    # If there aren't any symmetries, this is a good way to find the HOMO.
                    HOMO = len(self.moenergies[0])-1
                    self.homos = [HOMO]

                # Convert to floats and append to moenergies, but sometimes Gaussian
                #  doesn't print correctly so test for ValueError (bug 1756789).
                part = line[28:]
                i = 0
                while i*10+4 < len(part):
                    s = part[i*10:(i+1)*10]
                    try:
                        x = utils.float(s)
                    except ValueError:
                        x = numpy.nan
                    self.moenergies[0].append(utils.convertor(x, "hartree", "eV"))
                    i += 1
                line = next(inputfile)

            # If, at this point, self.homos is unset, then there were not
            # any alpha virtual orbitals
            if not hasattr(self, "homos"):
                HOMO = len(self.moenergies[0])-1
                self.homos = [HOMO]

            if line.find('Beta') == 2:
                self.moenergies.append([])

            HOMO = -2
            while line.find('Beta') == 2:
                if line.split()[1] == "virt." and HOMO == -2:

                    # If there aren't any symmetries, this is a good way to find the HOMO.
                    # Also, check for consistency if homos was already parsed.
                    HOMO = len(self.moenergies[1])-1
                    self.homos.append(HOMO)

                part = line[28:]
                i = 0
                while i*10+4 < len(part):
                    x = part[i*10:(i+1)*10]
                    self.moenergies[1].append(utils.convertor(utils.float(x), "hartree", "eV"))
                    i += 1
                line = next(inputfile)

            self.moenergies = [numpy.array(x, "d") for x in self.moenergies]

        # Start of the IR/Raman frequency section.
        # Caution is advised here, as additional frequency blocks
        #   can be printed by Gaussian (with slightly different formats),
        #   often doubling the information printed.
        # See, for a non-standard exmaple, regression Gaussian98/test_H2.log
        # If either the Gaussian freq=hpmodes keyword or IOP(7/33=1) is used,
        # an extra frequency block with higher-precision vibdisps is
        # printed before the normal frequency block.
        # Note that the code parses only the vibsyms and vibdisps
        # from the high-precision block, but parses vibsyms, vibfreqs, vibfconsts,
        # vibramans, vibrmasses and vibirs from the normal block. vibsyms parsed
        # from the high-precision block are discarded and replaced by those
        # from the normal block while the high-precision vibdisps, if present,
        # are used to overwrite default-precision vibdisps at the end of the parse.
        if line[1:14] == "Harmonic freq":  # This matches in both freq block types

            self.updateprogress(inputfile, "Frequency Information", self.fupdate)

            # The whole block should not have any blank lines.
            while line.strip() != "":

                # The line with indices
                if line[1:15].strip() == "" and line[15:60].split()[0].isdigit():
                    freqbase = int(line[15:60].split()[0])
                    if freqbase == 1 and hasattr(self, 'vibsyms'):
                        # we are coming accross duplicated information.
                        # We might be be parsing a default-precision block having
                        # already parsed (only) vibsyms and displacements from
                        # the high-precision block, or might be encountering
                        # a second low-precision block (see e.g. 25DMF_HRANH.log
                        # regression).
                        self.vibsyms = []
                        if hasattr(self, "vibirs"):
                            self.vibirs = []
                        if hasattr(self, 'vibfreqs'):
                            self.vibfreqs = []
                        if hasattr(self, 'vibramans'):
                            self.vibramans = []
                        if hasattr(self, "vibrmasses"):
                            self.vibrmasses = []
                        if hasattr(self, "vibfconsts"):
                            self.vibfconsts = []
                        if hasattr(self, 'vibdisps'):
                            self.vibdisps = []

                # Lines with symmetries and symm. indices begin with whitespace.
                if line[1:15].strip() == "" and not line[15:60].split()[0].isdigit():

                    if not hasattr(self, 'vibsyms'):
                        self.vibsyms = []
                    syms = line.split()
                    self.vibsyms.extend(syms)

                if line[1:15] == "Frequencies --":  # note: matches low-precision block, and

                    if not hasattr(self, 'vibfreqs'):
                        self.vibfreqs = []

                    freqs = [utils.float(f) for f in line[15:].split()]
                    self.vibfreqs.extend(freqs)

                if line[1:15] == "Red. masses --":  # note: matches only low-precision block

                    if not hasattr(self, 'vibrmasses'):
                        self.vibrmasses = []

                    rmasses = [utils.float(f) for f in line[15:].split()]
                    self.vibrmasses.extend(rmasses)

                if line[1:15] == "Frc consts  --":  # note: matches only low-precision block

                    if not hasattr(self, 'vibfconsts'):
                        self.vibfconsts = []

                    fconsts = [utils.float(f) for f in line[15:].split()]
                    self.vibfconsts.extend(fconsts)

                if line[1:15] == "IR Inten    --":  # note: matches only low-precision block

                    if not hasattr(self, 'vibirs'):
                        self.vibirs = []

                    irs = []
                    for ir in line[15:].split():
                        try:
                            irs.append(utils.float(ir))
                        except ValueError:
                            irs.append(utils.float('nan'))
                    self.vibirs.extend(irs)

                if line[1:15] == "Raman Activ --":  # note: matches only low-precision block

                    if not hasattr(self, 'vibramans'):
                        self.vibramans = []

                    ramans = []
                    for raman in line[15:].split():
                        try:
                            ramans.append(utils.float(raman))
                        except ValueError:
                            ramans.append(utils.float('nan'))

                    self.vibramans.extend(ramans)

                # Block with (default-precision) displacements should start with this.
                #                     1                      2                      3
                #                     A                      A                      A
                # Frequencies --   370.7936               370.7987               618.0103
                # Red. masses --     2.3022                 2.3023                 1.9355
                # Frc consts  --     0.1865                 0.1865                 0.4355
                # IR Inten    --     0.0000                 0.0000                 0.0000
                #  Atom  AN      X      Y      Z        X      Y      Z        X      Y      Z
                #     1   6     0.00   0.00  -0.04     0.00   0.00   0.19     0.00   0.00   0.12
                #     2   6     0.00   0.00   0.19     0.00   0.00  -0.06     0.00   0.00  -0.12

                if line.strip().split()[0:3] == ["Atom", "AN", "X"]:
                    if not hasattr(self, 'vibdisps'):
                        self.vibdisps = []
                    disps = []
                    if not hasattr(self, 'nqmf'):
                        self.set_attribute('nqmf', self.natom)
                    for n in range(self.nqmf):
                        line = next(inputfile)
                        numbers = [float(s) for s in line[10:].split()]
                        N = len(numbers) // 3
                        if not disps:
                            for n in range(N):
                                disps.append([])
                        for n in range(N):
                            disps[n].append(numbers[3*n:3*n+3])
                    self.vibdisps.extend(disps)

                # Block with high-precision (freq=hpmodes) displacements should start with this.
                #                           1         2         3         4         5
                #                           A         A         A         A         A
                #       Frequencies ---   370.7936  370.7987  618.0103  647.7864  647.7895
                #    Reduced masses ---     2.3022    2.3023    1.9355    6.4600    6.4600
                #   Force constants ---     0.1865    0.1865    0.4355    1.5971    1.5972
                #    IR Intensities ---     0.0000    0.0000    0.0000    0.0000    0.0000
                # Coord Atom Element:
                #   1     1     6          0.00000   0.00000   0.00000  -0.18677   0.05592
                #   2     1     6          0.00000   0.00000   0.00000   0.28440   0.21550
                #   3     1     6         -0.04497   0.19296   0.11859   0.00000   0.00000
                #   1     2     6          0.00000   0.00000   0.00000   0.03243   0.37351
                #   2     2     6          0.00000   0.00000   0.00000   0.14503  -0.06117
                #   3     2     6          0.18959  -0.05753  -0.11859   0.00000   0.00000
                if line.strip().split()[0:3] == ["Coord", "Atom", "Element:"]:
                    # Wait until very end of parsing to assign vibdispshp to self.vibdisps
                    # as otherwise the higher precision displacements will be overwritten
                    # by low precision displacements which are printed further down file
                    if not hasattr(self, 'vibdispshp'):
                        self.vibdispshp = []

                    disps = []
                    for n in range(3*self.natom):
                        line = next(inputfile)
                        numbers = [float(s) for s in line[16:].split()]
                        atomindex = int(line[4:10])-1  # atom index, starting at zero
                        numbermodes = len(numbers)

                        if not disps:
                            for mode in range(numbermodes):
                                # For each mode, make list of list [atom][coord_index]
                                disps.append([[] for x in range(0, self.natom)])
                        for mode in range(numbermodes):
                            disps[mode][atomindex].append(numbers[mode])
                    self.vibdispshp.extend(disps)

                line = next(inputfile)
                
        # Metadata for excited states methods
        #
        # For HF/DFT level ES, this is our trigger line:
        #  MDV=  1342177280 DFT=T DoStab=F Mixed=T DoRPA=F DoScal=F NonHer=F
        # Comparing DFT=T/F and DoRPA=T/F allows us to distinguish CIS, RPA, TD-DFT and TDA.
        if "MDV=" in line and "DFT=" in line and "DoRPA=" in line:
            if "DFT=T" in line:
                if "RPA=T" in line:
                    method = "TD-DFT"
                
                else:
                    method = "TDA"
            
            else:
                if "RPA=T" in line:
                    method = "RPA"
                
                else:
                    method = "CIS"
            
            self.metadata["excited_states_method"] = method
        
        if line.strip() == "EOM-CCSD":
            self.metadata['excited_states_method'] = "EOM-CCSD"
        
        # Electronic transitions.
        if line[1:14] == "Excited State":

            # Excited State 1:
            et_index = float(line.split()[2][:-1])

            if not hasattr(self, "etenergies") \
                or et_index <= self.last_et:
                self.etenergies = []
                self.etoscs = []
                self.etsyms = []
                self.etsecs = []
            
            # Keep track of the highest excited state, so we can detect when we enter a new
            # section (the 'highest' excited state will be the same or lower as the last one).
            self.last_et = et_index

            # Need to deal with lines like:
            # (restricted calc)
            # Excited State   1:   Singlet-BU     5.3351 eV  232.39 nm  f=0.1695
            # (unrestricted calc) (first excited state is 2!)
            # Excited State   2:   ?Spin  -A      0.1222 eV 10148.75 nm  f=0.0000
            # (Gaussian 09 ZINDO)
            # Excited State   1:      Singlet-?Sym    2.5938 eV  478.01 nm  f=0.0000  <S**2>=0.000
            p = re.compile(r":(?P<sym>.*?)(?P<energy>-?\d*\.\d*) eV")
            groups = p.search(line).groups()
            self.etenergies.append(utils.convertor(utils.float(groups[1]), "eV", "wavenumber"))
            self.etoscs.append(utils.float(line.split("f=")[-1].split()[0]))
            # Fix Gaussian's weird capitalisation.
            mult, symm = groups[0].strip().split("-")
            self.append_attribute("etsyms", "{}-{}".format(mult, self.normalisesym(symm)))

            line = next(inputfile)

            p = re.compile(r"(\d+)")
            CIScontrib = []
            while line.find(" ->") >= 0:  # This is a contribution to the transition
                parts = line.split("->")
                self.logger.debug(parts)
                # Has to deal with lines like:
                #       32 -> 38         0.04990
                #      35A -> 45A        0.01921
                frommoindex = 0  # For restricted or alpha unrestricted
                fromMO = parts[0].strip()
                if fromMO[-1] == "B":
                    frommoindex = 1  # For beta unrestricted
                fromMO = int(p.match(fromMO).group())-1  # subtract 1 so that it is an index into moenergies

                t = parts[1].split()
                tomoindex = 0
                toMO = t[0]
                if toMO[-1] == "B":
                    tomoindex = 1
                toMO = int(p.match(toMO).group())-1  # subtract 1 so that it is an index into moenergies

                percent = utils.float(t[1])
                # For restricted calculations, the percentage will be corrected
                # after parsing (see after_parsing() above).
                CIScontrib.append([(fromMO, frommoindex), (toMO, tomoindex), percent])
                line = next(inputfile)
            self.etsecs.append(CIScontrib)
            
            # Skip over 'de-excitation' contributions (these are typically hidden but can be revealed
            # by iop(9/40=2)).
            while line.find(" <-") >= 0:
                # These are not processed atm.
                line = next(inputfile)
            
            # Check if this state is our 'state of interest' (for optimisations etc).
            if "This state for optimization and/or second-order correction" in line:
                # Index to the current excited state.
                self.metadata['opt_state'] = len(self.etenergies) -1

        # Electronic transition transition-dipole data
        #
        # Ground to excited state transition electric dipole moments (Au):
        #       state          X           Y           Z        Dip. S.      Osc.
        #         1         0.0008     -0.0963      0.0000      0.0093      0.0005
        #         2         0.0553      0.0002      0.0000      0.0031      0.0002
        #         3         0.0019      2.3193      0.0000      5.3790      0.4456
        # Ground to excited state transition velocity dipole moments (Au):
        #       state          X           Y           Z        Dip. S.      Osc.
        #         1        -0.0001      0.0032      0.0000      0.0000      0.0001
        #         2        -0.0081      0.0000      0.0000      0.0001      0.0005
        #         3        -0.0002     -0.2692      0.0000      0.0725      0.3887
        # Ground to excited state transition magnetic dipole moments (Au):
        #       state          X           Y           Z
        #         1         0.0000      0.0000     -0.0003
        #         2         0.0000      0.0000      0.0000
        #         3         0.0000      0.0000      0.0035
        #
        # NOTE: In Gaussian03, there were some inconsitancies in the use of
        # small / capital letters: e.g.
        # Ground to excited state Transition electric dipole moments (Au)
        # Ground to excited state transition velocity dipole Moments (Au)
        # so to look for a match, we will lower() everything.
        #
        # EOM-CCSD looks similar, but has more data.
        #  ==============================================
        # 
        #          EOM-CCSD transition properties        
        # 
        #  ==============================================
        #  Ground to excited state transition electric dipole moments (Au):
        #        state          X           Y           Z        Dip. S.      Osc.
        #          1         0.1021     -0.0000     -0.0000      0.0107      0.0030
        #          2         0.0000      0.0000      0.0000      0.0000      0.0000
        #  Excited to ground state transition electric dipole moments (Au):
        #        state          X           Y           Z        Dip. S.      Osc.
        #          1         0.1046     -0.0000     -0.0000      0.0107      0.0030
        #          2         0.0000      0.0000      0.0000      0.0000      0.0000
        #  Ground to excited state transition velocity dipole moments (Au):
        #        state          X           Y           Z        Dip. S.      Osc.
        #          1        -0.1351     -0.0000     -0.0000      0.0186      0.0296
        #          2         0.0000      0.0000      0.0000      0.0000      0.0000
        #  Excited to ground state transition velocity dipole moments (Au):
        #        state          X           Y           Z        Dip. S.      Osc.
        #          1        -0.1378     -0.0000     -0.0000      0.0186      0.0296
        #          2        -0.0000      0.0000      0.0000      0.0000      0.0000
        #  Ground to excited state transition magnetic dipole moments (Au):
        #        state          X           Y           Z
        #          1         0.0000      0.5361      0.0000
        #          2        -0.0000      0.0000      0.6910
        #  Excited to ground state transition magnetic dipole moments (Au):
        #        state          X           Y           Z
        #          1         0.0000      0.5473      0.0000
        #          2        -0.0000     -0.0000      0.7057

        if line[1:51].lower() == "ground to excited state transition electric dipole":
            # In EOM-CCSD we have multiple levels of theory, so we always want to reset.
            self.etdips = []
            self.etveldips = []
            self.etmagdips = []

            # now loop over lines reading eteltrdips until we find eteltrdipvel
            line = next(inputfile)  # state          X ...
            line = next(inputfile)  # 1        -0.0001 ...
            
            # Older versions have fewer fields.
            while len(line.split()) in [5,6]:
                self.etdips.append(list(map(float, line.split()[1:4])))
                line = next(inputfile)
                
        if line[1:51].lower() == "ground to excited state transition velocity dipole":
            # now loop over lines reading etveleltrdips until we find
            # etmagtrdip
            line = next(inputfile)  # state          X ...
            line = next(inputfile)  # 1        -0.0001 ...
            
            # Older versions have fewer fields.
            while len(line.split()) in [5,6]:
                self.etveldips.append(list(map(float, line.split()[1:4])))
                line = next(inputfile)
                
        if line[1:51].lower() == "ground to excited state transition magnetic dipole":

            # now loop over lines while the line starts with at least 3 spaces
            line = next(inputfile)  # state          X ...
            line = next(inputfile)  # 1        -0.0001 ...
            while len(line.split()) == 4:
                self.etmagdips.append(list(map(float, line.split()[1:4])))
                line = next(inputfile)

        # Circular dichroism data (different for G03 vs G09)
        #
        # G03
        #
        # ## <0|r|b> * <b|rxdel|0>  (Au), Rotatory Strengths (R) in
        # ## cgs (10**-40 erg-esu-cm/Gauss)
        # ##       state          X           Y           Z     R(length)
        # ##         1         0.0006      0.0096     -0.0082     -0.4568
        # ##         2         0.0251     -0.0025      0.0002     -5.3846
        # ##         3         0.0168      0.4204     -0.3707    -15.6580
        # ##         4         0.0721      0.9196     -0.9775     -3.3553
        #
        # G09
        #
        # ## 1/2[<0|r|b>*<b|rxdel|0> + (<0|rxdel|b>*<b|r|0>)*]
        # ## Rotatory Strengths (R) in cgs (10**-40 erg-esu-cm/Gauss)
        # ##       state          XX          YY          ZZ     R(length)     R(au)
        # ##         1        -0.3893     -6.7546      5.7736     -0.4568     -0.0010
        # ##         2       -17.7437      1.7335     -0.1435     -5.3845     -0.0114
        # ##         3       -11.8655   -297.2604    262.1519    -15.6580     -0.0332
        if line[1:52] == "<0|r|b> * <b|rxdel|0>  (Au), Rotatory Strengths (R)" or \
           line[1:50] == "1/2[<0|r|b>*<b|rxdel|0> + (<0|rxdel|b>*<b|r|0>)*]":

            self.etrotats = []

            self.skip_lines(inputfile, ['units'])

            headers = next(inputfile)
            Ncolms = len(headers.split())
            line = next(inputfile)
            parts = line.strip().split()
            while len(parts) == Ncolms:
                try:
                    R = utils.float(parts[4])
                except ValueError:
                    # nan or -nan if there is no first excited state
                    # (for unrestricted calculations)
                    pass
                else:
                    self.etrotats.append(R)
                line = next(inputfile)
                temp = line.strip().split()
                parts = line.strip().split()
            self.etrotats = numpy.array(self.etrotats, "d")

        # Number of basis sets functions.
        # Has to deal with lines like:
        #  NBasis =   434 NAE=    97 NBE=    97 NFC=    34 NFV=     0
        # and...
        #  NBasis = 148  MinDer = 0  MaxDer = 0
        # Although the former is in every file, it doesn't occur before
        #   the overlap matrix is printed.
        if line[1:7] == "NBasis" or line[4:10] == "NBasis":

            # For counterpoise fragment, skip these lines.
            if self.counterpoise != 0:
                return

            # For ONIOM calcs, ignore this section in order to bypass assertion failure.
            if self.oniom:
                return

            # If nbasis was already parsed, check if it changed. If it did, issue a warning.
            # In the future, we will probably want to have nbasis, as well as nmo below,
            # as a list so that we don't need to pick one value when it changes.
            nbasis = int(line.split('=')[1].split()[0])
            if hasattr(self, "nbasis"):
                try:
                    assert nbasis == self.nbasis
                except AssertionError:
                    self.logger.warning(
                        f"Number of basis functions (nbasis) has changed from {int(self.nbasis)} to {int(nbasis)}"
                    )
            self.nbasis = nbasis

        # Number of linearly-independent basis sets.
        if line[1:7] == "NBsUse":

            # For counterpoise fragment, skip these lines.
            if self.counterpoise != 0:
                return

            # For ONIOM calcs, ignore this section in order to bypass assertion failure.
            if self.oniom:
                return

            nmo = int(line.split('=')[1].split()[0])
            self.set_attribute('nmo', nmo)

        # For AM1 calculations, set nbasis by a second method,
        #   as nmo may not always be explicitly stated.
        if line[7:22] == "basis functions, ":

            nbasis = int(line.split()[0])
            self.set_attribute('nbasis', nbasis)

        # Molecular orbital overlap matrix.
        # Has to deal with lines such as:
        #   *** Overlap ***
        #   ****** Overlap ******
        # Note that Gaussian sometimes drops basis functions,
        #  causing the overlap matrix as parsed below to not be
        #  symmetric (which is a problem for population analyses, etc.)
        if line[1:4] == "***" and (line[5:12] == "Overlap" or line[8:15] == "Overlap"):

            # Ensure that this is the main calc and not a fragment
            if self.counterpoise != 0:
                return

            self.aooverlaps = numpy.zeros((self.nbasis, self.nbasis), "d")
            # Overlap integrals for basis fn#1 are in aooverlaps[0]
            base = 0
            colmNames = next(inputfile)
            while base < self.nbasis:

                self.updateprogress(inputfile, "Overlap", self.fupdate)

                for i in range(self.nbasis-base):  # Fewer lines this time
                    line = next(inputfile)
                    parts = line.split()
                    for j in range(len(parts)-1):  # Some lines are longer than others
                        k = float(parts[j+1].replace("D", "E"))
                        self.aooverlaps[base+j, i+base] = k
                        self.aooverlaps[i+base, base+j] = k
                base += 5
                colmNames = next(inputfile)
            self.aooverlaps = numpy.array(self.aooverlaps, "d")

        # Molecular orbital coefficients (mocoeffs).
        # Essentially only produced for SCF calculations.
        # This is also the place where aonames and atombasis are parsed.
        if line[5:35] == "Molecular Orbital Coefficients" or line[5:41] == "Alpha Molecular Orbital Coefficients" or line[5:40] == "Beta Molecular Orbital Coefficients":

            # If counterpoise fragment, return without parsing orbital info
            if self.counterpoise != 0:
                return
            # Skip this for ONIOM calcs
            if self.oniom:
                return

            if line[5:40] == "Beta Molecular Orbital Coefficients":
                beta = True
                if self.popregular:
                    return
                    # This was continue before refactoring the parsers.
                    #continue # Not going to extract mocoeffs
                # Need to add an extra array to self.mocoeffs
                self.mocoeffs.append(numpy.zeros((self.nmo, self.nbasis), "d"))
            else:
                beta = False
                self.aonames = []
                self.atombasis = []
                mocoeffs = [numpy.zeros((self.nmo, self.nbasis), "d")]

            base = 0
            self.popregular = False
            for base in range(0, self.nmo, 5):

                self.updateprogress(inputfile, "Coefficients", self.fupdate)

                colmNames = next(inputfile)
                
                if any(name in colmNames for name in ["Density Matrix:", "DENSITY MATRIX.", "Beta Molecular Orbital Coefficients"]):
                    # Reached end of mocoeff section early, this implies pop was not full.
                    self.popregular = True
                    # We can stop processing.
                    break

                if not colmNames.split():
                    self.logger.warning("Molecular coefficients header found but no coefficients.")
                    break

                symmetries = next(inputfile)
                eigenvalues = next(inputfile)
                for i in range(self.nbasis):

                    line = next(inputfile)
                    if i == 0:
                        # Find location of the start of the basis function name
                        start_of_basis_fn_name = line.find(line.split()[3]) - 1
                    if base == 0 and not beta:  # Just do this the first time 'round
                        parts = line[:start_of_basis_fn_name].split()
                        if len(parts) > 1:  # New atom
                            if i > 0:
                                self.atombasis.append(atombasis)
                            atombasis = []
                            atomname = f"{parts[2]}{parts[1]}"
                        orbital = line[start_of_basis_fn_name:20].strip()
                        self.aonames.append(f"{atomname}_{orbital}")
                        atombasis.append(i)

                    part = line[21:].replace("D", "E").rstrip()
                    temp = []
                    for j in range(0, len(part), 10):
                        temp.append(float(part[j:j+10]))
                    if beta:
                        self.mocoeffs[1][base:base + len(part) // 10, i] = temp
                    else:
                        mocoeffs[0][base:base + len(part) // 10, i] = temp

                if base == 0 and not beta:  # Do the last update of atombasis
                    self.atombasis.append(atombasis)
            if not self.popregular and not beta:
                self.mocoeffs = mocoeffs

        # Natural orbital coefficients (nocoeffs) and occupation numbers (nooccnos),
        # which are respectively define the eigenvectors and eigenvalues of the
        # diagonalized one-electron density matrix. These orbitals are formed after
        # configuration interaction (CI) calculations, but not only. Similarly to mocoeffs,
        # we can parse and check aonames and atombasis here.
        #
        #     Natural Orbital Coefficients:
        #                           1         2         3         4         5
        #     Eigenvalues --     2.01580   2.00363   2.00000   2.00000   1.00000
        #   1 1   O  1S          0.00000  -0.15731  -0.28062   0.97330   0.00000
        #   2        2S          0.00000   0.75440   0.57746   0.07245   0.00000
        # ...
        #
        def natural_orbital_single_spin_parsing(inputfile, updateprogress_title):
            coeffs = numpy.zeros((self.nmo, self.nbasis), "d")
            occnos = []
            aonames = []
            atombasis = []
            for base in range(0, self.nmo, 5):
                self.updateprogress(inputfile, updateprogress_title, self.fupdate)
                colmNames = next(inputfile)
                eigenvalues = next(inputfile)
                occnos.extend(map(float, eigenvalues.split()[2:]))
                for i in range(self.nbasis):
                    line = next(inputfile)
                    # Just do this the first time 'round.
                    if base == 0:
                        parts = line[:12].split()
                        # New atom.
                        if len(parts) > 1:
                            if i > 0:
                                atombasis.append(basisonatom)
                            basisonatom = []
                            atomname = f"{parts[2]}{parts[1]}"
                        orbital = line[11:20].strip()
                        aonames.append(f"{atomname}_{orbital}")
                        basisonatom.append(i)
                    part = line[21:].replace("D", "E").rstrip()
                    temp = []
                    for j in range(0, len(part), 10):
                        temp.append(float(part[j:j+10]))
                    coeffs[base:base + len(part) // 10, i] = temp
                # Do the last update of atombasis.
                if base == 0:
                    atombasis.append(basisonatom)
            return occnos, coeffs, aonames, atombasis

        if line[5:33] == "Natural Orbital Coefficients":
            updateprogress_title = "Natural orbitals"
            nooccnos, nocoeffs, aonames, atombasis = natural_orbital_single_spin_parsing(inputfile, updateprogress_title)
            self.set_attribute("nocoeffs", nocoeffs)
            self.set_attribute("nooccnos", nooccnos)
            self.set_attribute("atombasis", atombasis)
            self.set_attribute("aonames", aonames)

        # Natural spin orbital coefficients (nsocoeffs) and occupation numbers (nsooccnos)
        # Parsed attributes are similar to the natural orbitals above except
        # the natural spin orbitals and occupation numbers are the eigenvalues
        # and eigenvectors of the one particles spin density matrices
        #     Alpha Natural Orbital Coefficients:
        #                           1         2         3         4         5
        #     Eigenvalues --     1.00000   1.00000   0.99615   0.99320   0.99107
        #   1 1   O  1S          0.70425   0.70600  -0.16844  -0.14996  -0.00000
        #   2        2S          0.01499   0.01209   0.36089   0.34940  -0.00000
        # ...
        #     Beta Natural Orbital Coefficients:
        #                           1         2         3         4         5
        #     Eigenvalues --     1.00000   1.00000   0.99429   0.98790   0.98506
        #   1 1   O  1S          0.70822   0.70798  -0.15316  -0.13458   0.00465
        #   2        2S          0.00521   0.00532   0.33837   0.33189  -0.01301
        #   3        3S         -0.02542  -0.00841   0.28649   0.53224   0.18902
        # ...

        if line[5:39] == "Alpha Natural Orbital Coefficients":
            updateprogress_title = "Natural Spin orbitals (alpha)"
            nsooccnos, nsocoeffs, aonames, atombasis = natural_orbital_single_spin_parsing(inputfile, updateprogress_title)
            if self.unified_no_nso:
                self.append_attribute("nocoeffs", nsocoeffs)
                self.append_attribute("nooccnos", nsooccnos)
            else:
                self.append_attribute("nsocoeffs", nsocoeffs)
                self.append_attribute("nsooccnos", nsooccnos)
            self.set_attribute("atombasis", atombasis)
            self.set_attribute("aonames", aonames)
        if line[5:38] == "Beta Natural Orbital Coefficients":
            updateprogress_title = "Natural Spin orbitals (beta)"
            nsooccnos, nsocoeffs, aonames, atombasis = natural_orbital_single_spin_parsing(inputfile, updateprogress_title)
            if self.unified_no_nso:
                self.append_attribute("nocoeffs", nsocoeffs)
                self.append_attribute("nooccnos", nsooccnos)
            else:
                self.append_attribute("nsocoeffs", nsocoeffs)
                self.append_attribute("nsooccnos", nsooccnos)
            self.set_attribute("atombasis", atombasis)
            self.set_attribute("aonames", aonames)

        # For FREQ=Anharm, extract anharmonicity constants
        if line[1:40] == "X matrix of Anharmonic Constants (cm-1)":
            Nvibs = len(self.vibfreqs)
            self.vibanharms = numpy.zeros((Nvibs, Nvibs), "d")

            base = 0
            colmNames = next(inputfile)
            while base < Nvibs:

                for i in range(Nvibs-base):  # Fewer lines this time
                    line = next(inputfile)
                    parts = line.split()
                    for j in range(len(parts)-1):  # Some lines are longer than others
                        k = float(parts[j+1].replace("D", "E"))
                        self.vibanharms[base+j, i+base] = k
                        self.vibanharms[i+base, base+j] = k
                base += 5
                colmNames = next(inputfile)

        # Pseudopotential charges.
        if line.find("Pseudopotential Parameters") > -1:

            self.skip_lines(inputfile, ['e', 'label1', 'label2', 'e'])

            line = next(inputfile)
            if line.find("Centers:") < 0:
                return
                # This was continue before parser refactoring.
                # continue

            # Needs to handle code like the following:
            #
            #  Center     Atomic      Valence      Angular      Power                                                       Coordinates
            #  Number     Number     Electrons     Momentum     of R      Exponent        Coefficient                X           Y           Z
            # ===================================================================================================================================
            # Centers:   1
            # Centers:  16
            # Centers:  21 24
            # Centers:  99100101102
            #    1         44           16                                                                      -4.012684 -0.696698  0.006750
            #                                      F and up
            #                                                     0      554.3796303       -0.05152700
            centers = []
            while line.find("Centers:") >= 0:
                temp = line[10:]
                for i in range(0, len(temp)-3, 3):
                    centers.append(int(temp[i:i+3]))
                line = next(inputfile)
            centers.sort()  # Not always in increasing order

            self.coreelectrons = numpy.zeros(self.natom, "i")

            for center in centers:
                front = line[:10].strip()
                while not (front and int(front) == center):
                    line = next(inputfile)
                    front = line[:10].strip()
                info = line.split()
                self.coreelectrons[center-1] = int(info[1]) - int(info[2])
                line = next(inputfile)

        # This will be printed for counterpoise calcualtions only.
        # To prevent crashing, we need to know which fragment is being considered.
        # Other information is also printed in lines that start like this.
        if line[1:14] == 'Counterpoise:':

            if line[42:50] == "fragment":
                self.counterpoise = int(line[51:54])

        # This will be printed only during ONIOM calcs; use it to set a flag
        # that will allow assertion failures to be bypassed in the code.
        if line[1:7] == "ONIOM:":
            self.oniom = True

        # This will be printed only during BOMD calcs;
        if line.startswith(" INPUT DATA FOR L118"):
            self.BOMD = True

        # Atomic charges are straightforward to parse, although the header
        # has changed over time somewhat.
        #
        # Mulliken charges:
        #                1
        #     1  C   -0.004513
        #     2  C   -0.077156
        # ...
        # Sum of Mulliken charges =   0.00000
        # Mulliken charges with hydrogens summed into heavy atoms:
        #               1
        #     1  C   -0.004513
        #     2  C    0.002063
        # ...
        #
        # Spins may be included in the same section.
        #
        # Mulliken charges and spin densities:
        #               1          2
        #     1  O   -0.596188   0.019133
        #     2  C    0.320624   0.000869
        #
        # APT and Lowdin charges are also displayed in this way.
        def extract_charges_spins(line,prop):
            """Extracts atomic charges and spin densities into 
               self.atomcharges and self.atomspins dictionaries.
    
            Inputs:
                line - line header marking the beginning of a
                particular set of charges or spins.
                prop - property type to be extracted as a
                string (e.g. Mulliken, Lowdin, APT).
            """
            has_spin = 'spin' in line.lower()
            has_charges = 'charges' in line.lower()
            if has_charges and not hasattr(self, "atomcharges"):
                self.atomcharges = {}
            if has_spin and not hasattr(self, "atomspins"):
                self.atomspins = {}
            _ = next(inputfile)
            charges = []
            spins = []
            is_sum = 'summed' in line
            # Iterate over each line and append values to a list 
            # based on whether they are charges or spins. 
            if is_sum:
                for i in self.atomnos:
                    # currently bug exists where files with translation vectors report
                    # an extra atom with atomnumber -2 in self.atomnos, so must ignore 
                    # this by passing whenever i in self.atomnos == -2.
                    if i == -2:
                        pass
                    # For lists of summed charges or spins, a value
                    # of 0 is added if the atom is a hydrogen.
                    elif i == 1:
                        if has_charges:
                            charges.append(float(0))
                            if has_spin:
                                spins.append(float(0))
                        elif has_spin:
                            spins.append(float(0))
                    else:
                        nline = next(inputfile)
                        # Some older versions of Gaussian already include 
                        # hydrogens with value 0 for summed charges or 
                        # spins, so these should be ignored.
                        while nline.split()[1] == "H":
                            nline = next(inputfile)
                        split_line = nline.split()
                        if has_charges:
                            charges.append(float(split_line[2]))
                            if has_spin:
                                spins.append(float(split_line[3]))
                        elif has_spin:
                            spins.append(float(split_line[2]))
            else:
                for i in self.atomnos:
                    # Ignore translation vectors.
                    if i == -2:
                        pass
                    else:
                        nline = next(inputfile)
                        split_line = nline.split()
                        if has_charges:
                            charges.append(float(split_line[2]))
                            if has_spin:
                                spins.append(float(split_line[3]))
                        elif has_spin:
                            spins.append(float(split_line[2]))
            # When the charge type is not given explicitly we 
            # must find it from the bottom line, which always 
            # has the format: "Sum of Mulliken charges=   0.00000"
            # so we can extract the type by splitting each 
            # line until we get a valid charge type.
            while prop.lower() not in ["mulliken","lowdin","apt"]:
                nline = next(inputfile)
                prop = nline.split()[2].lower()
            # Input extracted values into self.atomcharges.
            if has_charges:
                if is_sum:
                    self.atomcharges[f"{prop}_sum"] = charges
                else:
                    self.atomcharges[f"{prop}"] = charges
            if has_spin:
                if is_sum:
                    self.atomspins[f"{prop}_sum"] = spins
                else:
                    self.atomspins[f"{prop}"] = spins

        # Define strings needed for line detection. Older Gaussian
        # versions don't always give the charge type explicitly,
        # so we must include "atomic" as a general term to catch
        # all other atomic charge or spin lines.
        props = ["mulliken","lowdin","apt","atomic"]
        headers = [" atomic charges:",
        " charges:",
        " charges with hydrogens summed into heavy atoms:",
        " atomic charges with hydrogens summed into heavy atoms:",
        " atomic spin densities:",
        " charges and spin densities with hydrogens summed into heavy atoms:",
        " charges and spin densities:"]
        
        if hasattr(self, "atomnos"):
        # Combine props and headers to find lines heading lists
        # of atom charges or spins.
            for prop in props:
                for header in headers:
                    if f"{prop}{header}".lower() in line.lower():
                        # When we use "atomic" as the property, only
                        # extract if the charge type isn't given explicity.
                        # This prevents us from reading some lines twice.
                        # e.g. "Mulliken atomic charges:" is caught by 
                        # "mulliken atomic charges:" and " atomic charges:"
                        if prop == "atomic":
                            if not "mulliken" in line.lower() and not "lowdin" in line.lower() and not "apt" in line.lower():
                                extract_charges_spins(line,prop)
                        else:
                            extract_charges_spins(line,prop)
                        
        if line.strip() == "Natural Population":
            if not hasattr(self, 'atomcharges'):
                self.atomcharges = {}
            if "natural" not in self.atomcharges:
                line1 = next(inputfile)
                line2 = next(inputfile)
                if line1.split()[0] == 'Natural' and line2.split()[2] == 'Charge':
                    dashes = next(inputfile)
                    charges = []
                    for i in range(self.natom):
                        nline = next(inputfile)
                        charges.append(float(nline.split()[2]))
                    self.atomcharges["natural"] = charges

        # Combined Hirshfeld/CM5 is different enough that we don't try and
        # reuse extract_charges_spins, at least for now.
        if "Hirshfeld charges, spin densities, dipoles, and CM5 charges" in line:
            if not hasattr(self, "atomcharges"):
                self.atomcharges = {}
            has_spins = len(self.homos) == 2
            if has_spins and not hasattr(self, "atomspins"):
                self.atomspins = {}
            self.skip_line(inputfile, "Q-H        S-H        Dx")
            line = next(inputfile)
            atomcharges_hirshfeld = []
            atomcharges_cm5 = []
            atomspins_hirshfeld = []
            while "Tot" not in line:
                tokens = line.split()
                atomcharges_hirshfeld.append(float(tokens[2]))
                atomcharges_cm5.append(float(tokens[7]))
                if has_spins:
                    atomspins_hirshfeld.append(float(tokens[3]))
                line = next(inputfile)
            self.atomcharges["hirshfeld"] = atomcharges_hirshfeld
            self.atomcharges["cm5"] = atomcharges_cm5
            if has_spins:
                self.atomspins["hirshfeld"] = atomspins_hirshfeld
            self.skip_lines(
                inputfile,
                ["Hirshfeld charges with hydrogens summed into heavy atoms:", "Q-H       Q-CM5"]
            )
            line = next(inputfile)
            atomcharges_hirshfeld_summed = []
            atomcharges_cm5_summed = []
            for i in self.atomnos:
                if i == 1:
                    atomcharges_hirshfeld_summed.append(0.0)
                    atomcharges_cm5_summed.append(0.0)
                else:
                    tokens = line.split()
                    atomcharges_hirshfeld_summed.append(float(tokens[2]))
                    atomcharges_cm5_summed.append(float(tokens[3]))
                    line = next(inputfile)
            self.atomcharges["hirshfeld_sum"] = atomcharges_hirshfeld_summed
            self.atomcharges["cm5_sum"] = atomcharges_cm5_summed

        #Extract Thermochemistry
        #Temperature   298.150 Kelvin.  Pressure   1.00000 Atm.
        #Zero-point correction=                           0.342233 (Hartree/
        #Thermal correction to Energy=                    0.
        #Thermal correction to Enthalpy=                  0.
        #Thermal correction to Gibbs Free Energy=         0.302940
        #Sum of electronic and zero-point Energies=           -563.649744
        #Sum of electronic and thermal Energies=              -563.636699
        #Sum of electronic and thermal Enthalpies=            -563.635755
        #Sum of electronic and thermal Free Energies=         -563.689037
        if "Zero-point correction" in line:
            self.set_attribute('zpve', float(line.split()[2]))
        if "Sum of electronic and thermal Enthalpies" in line:
            self.set_attribute('enthalpy', float(line.split()[6]))
        if "Sum of electronic and thermal Free Energies=" in line:
            self.set_attribute('freeenergy', float(line.split()[7]))
        if line[1:13] == "Temperature ":
            self.set_attribute('temperature', float(line.split()[1]))
            self.set_attribute('pressure', float(line.split()[4]))

        # Static polarizability (from `polar`), lower triangular
        # matrix.
        if line[1:26] == "SCF Polarizability for W=":
            self.hp_polarizabilities = True
            if not hasattr(self, 'polarizabilities'):
                self.polarizabilities = []
            polarizability = numpy.zeros(shape=(3, 3))
            self.skip_line(inputfile, 'directions')
            for i in range(3):
                line = next(inputfile)
                polarizability[i, :i+1] = [utils.float(x) for x in line.split()[1:]]

            polarizability = utils.symmetrize(polarizability, use_triangle='lower')
            self.polarizabilities.append(polarizability)

        # Static polarizability (from `freq`), lower triangular matrix.
        if line[1:16] == "Polarizability=":
            self.hp_polarizabilities = True
            if not hasattr(self, 'polarizabilities'):
                self.polarizabilities = []
            polarizability = numpy.zeros(shape=(3, 3))
            polarizability_list = []
            polarizability_list.extend([line[16:31], line[31:46], line[46:61]])
            line = next(inputfile)
            polarizability_list.extend([line[16:31], line[31:46], line[46:61]])
            indices = numpy.tril_indices(3)
            polarizability[indices] = [utils.float(x) for x in polarizability_list]
            polarizability = utils.symmetrize(polarizability, use_triangle='lower')
            self.polarizabilities.append(polarizability)

        # Static polarizability, compressed into a single line from
        # terse printing.
        # Order is XX, YX, YY, ZX, ZY, ZZ (lower triangle).
        if line[2:23] == "Exact polarizability:":
            if not self.hp_polarizabilities:
                if not hasattr(self, 'polarizabilities'):
                    self.polarizabilities = []
                polarizability = numpy.zeros(shape=(3, 3))
                indices = numpy.tril_indices(3)
                try:
                    # G16 C01 changes polarizability printing
                    # Sample:
                    #       Exact polarizability:      68.238       6.777     143.018       0.000       0.000      11.343
                    polarizability[indices] = [utils.float(x) for x in 
                                               [line[23:35], line[35:47], line[47:59], line[59:71], line[71:83], line[83:95]]]
                except:
                    # G16A03 and older
                    # Sample:
                    #       Exact polarizability:  68.238  -6.777 143.018   0.000   0.000  11.343
                    polarizability[indices] = [utils.float(x) for x in 
                                               [line[23:31], line[31:39], line[39:47], line[47:55], line[55:63], line[63:71]]]
                polarizability = utils.symmetrize(polarizability, use_triangle='lower')
                self.polarizabilities.append(polarizability)

        # IRC Computation convergence checks.
        #
        # -------- Sample extract for IRC step --------
        #
        # IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC
        # ------------------------------------------------------------------------
        # INPUT DATA FOR L123
        # ------------------------------------------------------------------------
        # GENERAL PARAMETERS:
        # Follow reaction path in both directions.
        # Maximum points per path      = 200
        # Step size                    =   0.100 bohr
        # Integration scheme           = HPC
        #    Redo corrector integration= Yes
        #    DWI Weight Power          =  2
        #    DWI will use Hessian update vectors when possible.
        #    Max correction cycles     =  50
        # Initial Hessian              = CalcFC
        # Hessian evaluation           = Analytic every   5 predictor steps
        #                              = Analytic every   5 corrector steps
        # Hessian updating method      = Bofill
        # ------------------------------------------------------------------------
        # IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC
        #
        # -------- Sample extract for converged step --------
        # IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC
        # Pt  1 Step number   1 out of a maximum of  50
        # Modified Bulirsch-Stoer Extrapolation Cycles:
        #    EPS =    0.000010000000000
        # Maximum DWI energy   std dev =  0.000000595 at pt     1
        # Maximum DWI gradient std dev =  0.135684493 at pt     2
        # CORRECTOR INTEGRATION CONVERGENCE:
        #   Recorrection delta-x convergence threshold:    0.010000
        #   Delta-x Convergence Met
        # Point Number:   1          Path Number:   1
        #   CHANGE IN THE REACTION COORDINATE =    0.16730
        #   NET REACTION COORDINATE UP TO THIS POINT =    0.16730
        #  # OF POINTS ALONG THE PATH =   1
        #  # OF STEPS =   1
        #
        # Calculating another point on the path.
        # IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC
        #
        # -------- Sample extract for unconverged intermediate step --------
        # IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC
        # Error in corrector energy =          -0.0000457166
        # Magnitude of corrector gradient =     0.0140183779
        # Magnitude of analytic gradient =      0.0144021969
        # Magnitude of difference =             0.0078709968
        # Angle between gradients (degrees)=   32.1199
        # Pt 40 Step number   2 out of a maximum of  20
        # Modified Bulirsch-Stoer Extrapolation Cycles:
        #    EPS =    0.000010000000000
        # Maximum DWI energy   std dev =  0.000007300 at pt    31
        # Maximum DWI gradient std dev =  0.085197906 at pt    59
        # CORRECTOR INTEGRATION CONVERGENCE:
        #   Recorrection delta-x convergence threshold:    0.010000
        #   Delta-x Convergence NOT Met
        # IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC-IRC
        if line[1:20] == "INPUT DATA FOR L123":  # First IRC step
            if not hasattr(self, "optstatus"):
                self.optstatus = []
            self.optstatus.append(data.ccData.OPT_NEW)
        if line[3:22] == "Delta-x Convergence":
            if line[23:30] == "NOT Met":
                self.optstatus[-1] += data.ccData.OPT_UNCONVERGED
            elif line[23:26] == "Met":
                self.optstatus[-1] += data.ccData.OPT_DONE
                if not hasattr(self, 'optdone'):
                    self.optdone = []
                self.optdone.append(len(self.optstatus) - 1)
                
        # Save num CPUs incase we have an old version of Gaussian which doesn't print wall times.
        if "Will use up to" in line:
            self.num_cpu = int(line.split()[4])

        # Extract total elapsed (wall) and CPU job times
        if line[:14] == ' Elapsed time:' or line[:14] == ' Job cpu time:':
            # create empty list for the times to be stored in
            if line[:14] == ' Elapsed time:' and not "wall_time" in self.metadata:
                self.metadata['wall_time'] = []
            if line[:14] == ' Job cpu time:' and not "cpu_time" in self.metadata:
                self.metadata['cpu_time'] = []
            # the line format is " Elapsed time:       0 days  0 hours  0 minutes 47.5 seconds." at the end of each job ran.
            # the line format is " Job cpu time:       0 days  0 hours  8 minutes 45.7 seconds." at the end of each job ran.
            try:
                n = 2
                key = 'wall_time'
                # if parsing a cpu time change key and shift n
                if line[:14] == ' Job cpu time:':
                    n += 1
                    key = 'cpu_time'
                # split the line by white space
                split_line = line.split()
                # cast the time elements as floats for use in timedelta data structure
                time = datetime.timedelta(
                    days=float(split_line[n]),
                    hours=float(split_line[n+2]),
                    minutes=float(split_line[n+4]),
                    seconds=float(split_line[n+6]),
                )
                self.metadata[key].append(time)
            except:
                pass

        # Extract Rotational Constants
        # Example:
        # Rotational constants (GHZ):           3.13081     1.24272     0.88960
        # OR for linear molecules:
        # Rotational constants (GHZ): ************ 12.73690 12.73690
        # Note: rotational constant will be converted to wavenumber units (1/cm) to standardize across parsers
        if line[:28] == ' Rotational constants (GHZ):':
            splits = line[28:].split()
            self.append_attribute("rotconsts", [float(splits[i]) for i in (-3, -2, -1)])

        # Extract Molecular Mass (in amu)
        # Example:
        # Molecular mass:   128.06260 amu.
        if line[:16] == ' Molecular mass:':
            splits = line.split()
            self.molmass = utils.float(splits[2])

        if line[:31] == ' Normal termination of Gaussian':
            self.metadata['success'] = True
