import numpy.linalg as la
import numpy as np
from numpy.typing import NDArray
import xyz_py as xyzp
import extto
import re
import pandas as pd
from io import StringIO
import pathlib
import scipy.constants as sc

# Reimplemented for convenience
DataNotFoundError = extto.DataNotFoundError
DataFormattingError = extto.DataFormattingError


class OrcaVersionExtractor(extto.LineExtractor):
    '''
    Extracts Orca version from output file
    '''

    # Regex Start Pattern
    PATTERN = rb'Program Version\s+\d\.\d\.\d'

    MODIFIERS = [re.IGNORECASE]

    @property
    def data(self) -> list[list[int]]:
        '''
        Orca version, one per match
        Version number is stored as [major, minor, patch]
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> list[int]:
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        int
            Number of cores
        '''

        version = re.findall(r'(\d\.\d\.\d)', block)[0]

        version = version.replace('.', '')

        version = [int(v) for v in version]

        return version

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[int]:
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[int]
            Orca version as [major, minor, patch]
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data[0]


def get_coords(file_name: str | pathlib.Path, coord_type: str = 'init',
               index_style: str = 'per_element') -> tuple[list, NDArray]:
    '''
    Extracts cartesian coordinates and atom labels from Orca output file

    Parameters
    ----------
    file_name: str | pathlib.Path
        Orca output file to parse
    coord_type: str, {'init', 'opt'}
        Specifies which set of coordinates to extract\n
        Options are:\n
        "init" = Initial coordinates\n
        "opt" = Final optimised coordinates
    index_style: str {'per_element', 'sequential', 'sequential_orca', 'none'}
        Specifies what type of atom label indexing used for final atom labels\n
        Options are:\n
        'per_element' = Index by element e.g. Dy1, Dy2, N1, N2, etc.\n
        'sequential' = Index the atoms 1->N regardless of element\n
        'sequential_orca' = Index the atoms 0->N-1 regardless of element\n
        'none' = No label indexing

    Returns
    -------
    list
        Atomic labels
    ndarray of floats
        (n_atoms,3) array containing xyz coordinates of each atom
    '''

    labels, coords = [], []

    with open(file_name, 'r') as f:
        for line in f:
            if 'CARTESIAN COORDINATES (ANGSTROEM)' in line:
                labels, coords = [], []
                line = next(f)
                line = next(f)
                while len(line.lstrip().rstrip()):
                    labels.append(line.split()[0])
                    coords.append([float(val) for val in line.split()[1:4]])
                    line = next(f)
                if coord_type.lower() == 'init':
                    break

    if not len(labels):
        raise ValueError(f'Cannot find coordinates in {file_name}')

    if index_style in ['per_element', 'sequential']:
        labels = xyzp.add_label_indices(labels, style=index_style)
    elif index_style == 'sequential_orca':
        labels = xyzp.add_label_indices(
            labels, style='sequential', start_index=0
        )
    else:
        labels = xyzp.remove_label_indices(labels)

    return labels, np.asarray(coords)


class SusceptibilityExtractor(extto.BetweenExtractor):
    '''
    Extracts Magnetic Susceptibility as a function of temperature
    '''

    # Modifiers for line/block extraction
    MODIFIERS = [re.MULTILINE]

    # Regex Start Pattern
    START_PATTERN = rb'(?<=-{59}$\s{8}TEMPERATURE DEPENDENT MAGNETIC SUSCEPTIBILITY\s{8}-{59}[\S\s]{182})' # noqa

    # Regex End Pattern
    END_PATTERN = rb'(?=-{59}$)'

    @property
    def data(self) -> list[pd.DataFrame]:
        '''
        Processed susceptibility data, one dataframe per extracted block.\n
        For each entry, column titles are \n
         - Static Field (Gauss)\n
         - Temperature (K)\n
         - M/B: chi*T (cm3*K/mol)\n
         - chi*T (cm3*K/mol)\n
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> pd.DataFrame:
        '''
        Converts single block into array of susceptibility as a function of
        temperature

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        pd.DataFrame
            Susceptibility dataframe with columns described in self.data
        '''

        _ext = re.compile(
            r'\s*(\d+\.\d+)\s+(\d+\.\d+)\s+(----|\d+\.\d+)\s+(\d+\.\d+)\s*'
        )

        data = pd.DataFrame(_ext.findall(block), index=None)
        data.columns = [
            'Static Field (Gauss)',
            'Temperature (K)',
            'M/B: chi*T (cm3*K/mol)',
            'chi*T (cm3*K/mol)'
        ]
        data['M/B: chi*T (cm3*K/mol)'] = pd.to_numeric(
            data['M/B: chi*T (cm3*K/mol)'],
            errors='coerce'
        )
        data = data.astype(float)

        return data

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[pd.DataFrame]:
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[pd.DataFrame]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class ExchangeCouplingExtractor(extto.BetweenExtractor):
    '''
    Extracts Exchange Coupling Constants and information (J) from output file
    '''

    # Regex Start Pattern
    START_PATTERN = rb'(?<=BROKEN SYMMETRY MAGNETIC COUPLING ANALYSIS\s-{42})' # noqa

    # Regex End Pattern
    END_PATTERN = rb'(?=Ginsberg)'

    @property
    def data(self) -> list[dict[str, float]]:
        '''
        Processed Exchange coupling analysis, one dict per block
        For each list entry, keys are \n
         - S(High-Spin)\n
         - <S**2>(High-Spin)\n
         - <S**2>(BrokenSym)\n
         - E(High-Spin) (Eh)\n
         - E(BrokenSym) (Eh)\n
         - E(High-Spin)-E(BrokenSym) (cm^-1)
         - J(1) (cm^-1)
         - J(2) (cm^-1)
         - J(3) (cm^-1)
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> dict[str, float]:
        '''
        Converts single block into array of susceptibility as a function of
        temperature

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        dict[str, float]
            Keys described in self.data
        '''

        data = {
            'S(High-Spin)': float(re.findall(
                r'S\(High-Spin\) *= *(\d*\.\d*)',
                block
            )[0]),
            '<S**2>(High-Spin)': float(re.findall(
                r'<S\*\*2>\(High-Spin\) *= *(\d*\.\d*)',
                block
            )[0]),
            '<S**2>(BrokenSym)': float(re.findall(
                r'<S\*\*2>\(BrokenSym\) *= *(\d*\.\d*)',
                block
            )[0]),
            'E(High-Spin) (Eh)': float(re.findall(
                r'E\(High-Spin\) *= *(-\d*\.\d*) Eh',
                block
            )[0]),
            'E(BrokenSym) (Eh)': float(re.findall(
                r'E\(BrokenSym\) *= *(-\d*\.\d*) Eh',
                block
            )[0]),
            'E(High-Spin)-E(BrokenSym) (cm^-1)': float(re.findall(
                r'E\(High-Spin\)-E\(BrokenSym\)= *-?\d*.\d* eV *(-?\d*\.\d*) *cm\*\*-1', # noqa
                block
            )[0]),
            'J(1) (cm^-1)': float(re.findall(
                r'J\(1\) *= *(-?\d*\.\d*)',
                block
            )[0]),
            'J(2) (cm^-1)': float(re.findall(
                r'J\(2\) *= *(-?\d*\.\d*)',
                block
            )[0]),
            'J(3) (cm^-1)': float(re.findall(
                r'J\(3\) *= *(-?\d*\.\d*)',
                block
            )[0])
        }

        return data

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[dict[str, float]]:
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[dict[str, float]]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class AILFTOrbEnergyExtractor(extto.BetweenExtractor):
    '''
    Extracts AI-LFT orbital energies from output file
    '''
    # Regex Start Pattern
    START_PATTERN = rb'(?<=The ligand field one electron eigenfunctions:\s-{41})' # noqa

    # Regex End Pattern
    END_PATTERN = rb'(?=Ligand field orbitals were stored in)'

    MODIFIERS = [re.MULTILINE]

    @property
    def data(self) -> list[dict[str, NDArray]]:
        '''
        AI-LFT one electron eigenvalues and eigenfunctions, one dict per block
        For each dict, keys are \n
         - energy (cm^-1)\n
         - eigenvectors\n
         - orbitals
        and all values are numpy arrays of shape (n_orbs, n_orbs)\n
        or just n_orbs\n
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> dict[str, NDArray]:
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        dict[str, NDArray]
            Keys described in self.data
        '''

        energies = re.findall(
            r'\s+\d\s+\d\.\d{3}\s+(\d+\.\d)',
            block
        )
        energies = np.array([float(energy) for energy in energies])

        n_orbs = len(energies)

        vectors = re.findall(
            rf'\s+\d\s+\d\.\d{{3}}\s+\d+\.\d((?:\s+\-?\d\.\d{{6}}){{{n_orbs:d}}})', # noqa
            block
        )

        vectors = np.array([
            [float(ve) for ve in vector.split()]
            for vector in vectors
        ]).T

        names = re.findall(
            rf'Orbital\s+Energy\s+\(eV\)\s+Energy\s?\(cm-1\)((?:\s+[A-Za-z\d-]*){{{n_orbs:d}}})', # noqa
            block
        )
        names = np.array([na.rstrip().lstrip() for na in names[0].split()])

        data = {
            'energies (cm^-1)': energies,
            'eigenvectors': vectors,
            'orbitals': names
        }

        return data

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[dict[str, NDArray]]: # noqa
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[dict[str, NDArray]]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class FrequencyExtractor(extto.BetweenExtractor):
    '''
    Extracts Vibrational mode energies, eigenvectors, and intensity \n
    information from output file
    '''

    # Regex Start Pattern
    START_PATTERN = rb'(?<=VIBRATIONAL FREQUENCIES\s-{23})'

    # Regex End Pattern
    END_PATTERN = rb'(?=The first frequency considered to be a vibration)'

    MODIFIERS = [re.MULTILINE]

    @property
    def data(self) -> list[dict[str, NDArray]]:
        '''
        Processed IR SPECTRUM data, one dict per block
        For each dict, keys are \n
         - frequency (cm^-1)\n
         - displacements - shape: (n_atoms, 3 * n_atoms, 3)\n
         - epsilon (L mol^-1 cm^-1)\n
         - IR Intensity (km mol^-1)\n
         - tx\n
         - ty\n
         - tz\n
        and all values are numpy arrays.\n
        Dimensions are n_atoms*3, 1 for all arrays other than displacements.
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> dict[str, NDArray]:
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        dict[str, NDArray]
            Keys described in self.data
        '''

        # Extract Frequencies (wavenumber)
        freq_pattern = re.compile(
            r' *\d*: *(-?\d*\.\d*) cm\*\*-1'
        )
        wavenumbers = np.array(
            [float(val) for val in freq_pattern.findall(block)]
        )

        n_atoms = len(wavenumbers) // 3

        # Extract Displacements
        disp_table = re.findall(
            r'((?: +\d+){1,6} +\s(?: +\d+ +(?: +-?\d\.\d{6}){1,6}\s)*)',
            block
        )

        # and combine in a single dataframe
        master = pd.read_csv(
                StringIO(disp_table[0]),
                sep=r'\s{2,}',
                engine='python',
                index_col=0
            )

        for chunk in disp_table[1:]:
            _df = pd.read_csv(
                StringIO(chunk),
                sep=r'\s{2,}',
                engine='python',
                index_col=0
            )
            master = master.join(_df)

        # convert to numpy array
        disp_table = master.to_numpy()
        # and reshape
        disp_x = disp_table[0::3, :]
        disp_y = disp_table[1::3, :]
        disp_z = disp_table[2::3, :]
        disp = np.zeros((n_atoms, n_atoms * 3, 3))
        disp[:, :, 0] = disp_x
        disp[:, :, 1] = disp_y
        disp[:, :, 2] = disp_z

        # Extract TX, TY, and TZ and calculate epsilon
        tx_pattern = re.compile(
            r'\(([ -]?\d\.\d{6}) *-?\d\.\d{6} *-?\d\.\d{6}\)'
        )
        ty_pattern = re.compile(
            r'\([ -]?\d\.\d{6} *(-?\d\.\d{6}) *-?\d\.\d{6}\)'
        )
        tz_pattern = re.compile(
            r'\([ -]?\d\.\d{6} *-?\d\.\d{6} *(-?\d\.\d{6})\)'
        )
        # t2_pattern = re.compile(
        #     r'(\d\.\d{6}) *\([ -]?\d\.\d{6} *-?\d\.\d{6} *-?\d\.\d{6}\)'
        # )
        # eps_pat = re.compile(
        #     r'\d{,4}: * \d{,4}.\d{2} * (\d.\d{6})'
        # )
        tx = np.asarray(
            [0] * 6 + [float(val) for val in tx_pattern.findall(block)]
        )
        ty = np.asarray(
            [0] * 6 + [float(val) for val in ty_pattern.findall(block)]
        )
        tz = np.asarray(
            [0] * 6 + [float(val) for val in tz_pattern.findall(block)]
        )

        # epso = np.asarray(
        #     [0] * 6 + [float(val) for val in eps_pat.findall(block)]
        # )

        # This is the T2 printed by ORCA
        # (Dipole derivative wrt to MWC)**2 * vibrational overlap**2
        # units of e^2 a0^2
        t2 = tx ** 2 + ty ** 2 + tz ** 2

        # Calculate T
        # units of e^2 a0^2
        t = np.sqrt(t2)

        # Remove vibrational overlap
        # in atomic units
        # b^2 = hbar/(2*c*nubar) --> 1/(2*nubar)
        # m_e^-1/2 a_0
        icm2eh = 100 / sc.physical_constants['hartree-inverse meter relationship'][0] # noqa
        b2 = 1 / (2 * icm2eh * wavenumbers)
        # divide by sqrt(b2)
        # to give units of e m_e^-1/2
        t /= np.sqrt(b2)

        # Convert to SI
        # First convert electrons to coulombs
        t *= sc.e

        # and then reciprocal atomic mass units (m_e^-1/2) to kg^-1/2
        t *= (sc.m_e)**-0.5

        # Calculate A_e in units of m mol^-1
        ae = sc.Avogadro / (12 * sc.epsilon_0 * sc.speed_of_light**2) * t ** 2
        # and convert to km mol^-1
        ae /= 1000

        # Convert absorbance to linear
        alin = ae / np.log(10)

        # Convert to units of 1000 cm mol^-1
        eps = alin * 100

        data = {
            'energy (cm^-1)': wavenumbers,
            'displacements': disp,
            'epsilon (L mol^-1 cm^-1)': eps,
            'IR Intensity (km mol^-1)': ae,
            't2 (a.u.^2)': t2,
            'tx (a.u.)': tx,
            'ty (a.u.)': ty,
            'tz (a.u.)': tz
        }

        return data

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[dict[str, NDArray]]: # noqa
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[dict[str, NDArray]]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class LoewdinPopulationExtractor(extto.BetweenExtractor):
    '''
    Extracts Loewdin Population Analysis Section from output file
    '''

    # Regex Start Pattern
    START_PATTERN = rb'(?<=LOEWDIN ATOMIC CHARGES AND SPIN DENSITIES\s-{41}\s)'

    # Regex End Pattern
    END_PATTERN = rb'(?=-{50})'

    @property
    def data(self) -> list[tuple[dict[str, float], dict[str, float]]]:
        '''
        Processed Loewdin Population Analysis data\n
        Each data entry is a tuple containing two dictionaries:\n\n

        First dict is charges, second is spin density. In both cases\n
        keys are atomic symbol with (0-)index post-appended e.g "Cr0" "H22"\n
        and values are float value of charge or spin density\n
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> tuple[dict[str, float], dict[str, float]]: # noqa
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        tuple[dict[str, float], dict[str, float]]
            First dict is charges, second is spin density. In both cases\n
            keys are atomic symbol with (0-)index post-appended\n
                e.g "Cr0" "H22"\n
            values are float value of charge or spin density\n
        '''

        raw = re.findall(
            r'\s*(\d+)\s*([A-Za-z]*)\s*:\s+(-?\d\.\d{6})\s*(-?\d\.\d{6})',
            block
        )
        charges = {
            f'{val[1]}{val[0]}': float(val[2])
            for val in raw
        }

        spins = {
            f'{val[1]}{val[0]}': float(val[3])
            for val in raw
        }

        return (charges, spins)

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[tuple[list[dict[str, float]], list[float]]]: # noqa
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[tuple[list[dict[str, float]], list[float]]]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class MullikenPopulationExtractorDensities(LoewdinPopulationExtractor):
    '''
    Extracts Mulliken Population Analysis Section with \n
    MULLIKEN ATOMIC CHARGES AND SPIN DENSITIES header\n
    from output file
    '''

    # Regex Start Pattern
    START_PATTERN = rb'(?<=MULLIKEN ATOMIC CHARGES AND SPIN DENSITIES\s-{42}\s)' # noqa

    # Regex End Pattern
    END_PATTERN = rb'(?=-{51})'


class MullikenPopulationExtractorPopulations(LoewdinPopulationExtractor):
    '''
    Extracts Mulliken Population Analysis Section with \n
    MULLIKEN ATOMIC CHARGES AND SPIN POPULATIONS header
    from output file
    '''

    # Regex Start Pattern
    START_PATTERN = rb'(?<=MULLIKEN ATOMIC CHARGES AND SPIN POPULATIONS\s-{44}\s)' # noqa

    # Regex End Pattern
    END_PATTERN = rb'(?=-{51})'


class MullikenPopulationExtractor(MullikenPopulationExtractorDensities):
    '''
    Extracts Mulliken Population Analysis Section from output file
    '''


class LoewdinCompositionExtractor(extto.BetweenExtractor):
    '''
    Extracts Loewdin Orbital-Compositions Section from output file
    '''

    # Regex Start Pattern
    START_PATTERN = rb'(?<=LOEWDIN ORBITAL-COMPOSITIONS\s-{28}\s)'

    # Regex End Pattern
    END_PATTERN = rb'(?=-{28})'

    @property
    def data(self) -> list[tuple[list[dict[str, float]], list[float]]]:
        '''
        Processed Loewdin Natural orbital data\n
        Each data entry is a tuple containing:\n
        First entry - a list of dicts, one dict per Natural orbital\n
            Keys:\n
                ATOM_NUMBER ATOM_SYMBOL ORBITAL_NAME\n
            Values\n
                % Contribution to orbital\n
        Second entry - a list of occupation numbers, one float per orbital\n
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> tuple[list[dict[str, float]], list[float]]: # noqa
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        tuple[list[dict[str, float]], list[float]]
            First entry - a list of dicts, one dict per Natural orbital\n
                Keys:\n
                    ATOM_NUMBER ATOM_SYMBOL ORBITAL_NAME\n
                Values\n
                    % Contribution to orbital\n
            Second entry - a list of occupation numbers, one float per orbital\n # noqa
        '''

        # Extract each <=5 orbital sub block
        patt = re.compile(
            r'\n{2,}'
        )
        _sub_blocks = patt.split(block)
        _sub_blocks = [
            sb.lstrip().rstrip()
            for sb in _sub_blocks
            if len(sb.lstrip().rstrip())
        ]

        # and process into lists
        occupations = []
        contributions = []

        for _sub_block in _sub_blocks:
            [_header, _, _table] = re.split(r'\s(:?--------\s+)+', _sub_block)

            _orb_nos = [
                int(val)
                for val in _header.split('\n')[0].split()
            ]

            _occs = [
                float(val)
                for val in _header.split('\n')[2].split()
            ]

            for _occ in _occs:
                occupations.append(_occ)

            _df = pd.read_csv(
                StringIO(_table), sep=r'\s+',
                header=None,
                engine='python',
                index_col=None
            )
            _df.set_index([0, 1, 2], inplace=True)
            _df.columns = _orb_nos

            for _on in _orb_nos:
                _contrib = {}
                for name, pc in zip(_df.index, _df[_on]):
                    if pc > 1.:
                        _contrib[f'{name[0]:3} {name[1]:2} {name[2]:5}'] = pc
                contributions.append(_contrib)

        return (contributions, occupations)

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[tuple[list[dict[str, float]], list[float]]]: # noqa
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[tuple[list[dict[str, float]], list[float]]]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class HessianExtractor(extto.BetweenExtractor):
    '''
    Extracts Hessian from .hess file
    '''

    # Modifiers for line/block extraction
    MODIFIERS = [re.MULTILINE]

    # Regex Start Pattern
    START_PATTERN = rb'(?<=\$hessian\n)'

    # Regex End Pattern
    END_PATTERN = rb'(?=^\s*$)'

    @property
    def data(self) -> list[NDArray]:
        '''
        Hessian matrix data, one matrix per block.\n
        Each matrix is a 3N x 3N ndarray of floats\n
        where N is the number of atoms
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> NDArray:
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        ndarray of floats
            Hessian matrix as 3N x 3N array of floats, where N is number of\n
            atoms
        '''

        # Extract each <=5 orbital sub block
        patt = re.compile(
            r'(\s{15,}\d{1,4}){1,5}'
        )
        _sub_blocks = patt.split(block)

        # Convert each {2,6} x 3N block into a list of floats and remove
        # row index
        _sub_blocks = [
            [
                [
                    float(val)
                    for it, val in enumerate(row.split())
                    if it
                ]
                for row in sb.lstrip().rstrip().split('\n')
                if len(row.split()) > 1
            ]
            for sb in _sub_blocks
            if len(sb.lstrip().rstrip().split('\n'))
        ]

        # Remove the empty lists
        _sub_blocks = [sb for sb in _sub_blocks if len(sb)]
        hessian = np.hstack(_sub_blocks, dtype=float)

        return hessian

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[NDArray]:
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[tuple[list[dict[str, float]], list[float]]]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class AbsorptionElectricDipoleExtractor(extto.BetweenExtractor):
    '''
    Extracts ABSORPTION SPECTRUM VIA TRANSITION ELECTRIC DIPOLE MOMENTS table\n
    from ORCA output file for versions newer than 6.
    '''

    # Regex Start Pattern
    START_PATTERN = rb'(?<=ABSORPTION SPECTRUM VIA TRANSITION ELECTRIC DIPOLE MOMENTS\s[\S\s]{408}\s)' # noqa

    # Regex End Pattern
    END_PATTERN = rb'(?=-{77})'

    @property
    def data(self) -> dict[str, list[str | float]]:
        '''
        Absorption spectrum data:\n
        A dictionary with keys:\n
            transition\n
            energy (cm^-1)\n
            energy (ev)\n
            wavelength (nm)\n
            fosc\n
            d2 (a.u.^2)\n
            dx (a.u.)\n
            dy (a.u.)\n
            dz (a.u.)\n
        All values are list[float], but 'transition' entries are list[str]
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> dict[str, list[int | float]]: # noqa
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        dict[str, list[float]]
        '''

        result = re.findall(
            r'\s+(\d+-\d+[A-Z]\s+->\s+\d+-\d+[A-Z])\s+(\d\.\d*)\s+(\d*\.\d)\s+(\d*\.\d)\s+(\d\.\d{9})\s+(\d\.\d{5})\s+(-*\d\.\d{5})\s+(-*\d\.\d{5})\s+(-*\d\.\d{5})', # noqa
            block
        )

        result = np.asarray(result, dtype=str).T

        fresult = result[1:].astype(float)

        data = {
            'state': result[0].tolist(),
            'energy (ev)': fresult[0].tolist(),
            'energy (cm^-1)': fresult[1].tolist(),
            'wavelength (nm)': fresult[2].tolist(),
            'fosc': fresult[3].tolist(),
            't2 (a.u.^2)': fresult[4].tolist(),
            'tx (a.u).': fresult[5].tolist(),
            'ty (a.u).': fresult[6].tolist(),
            'tz (a.u).': fresult[7].tolist()
        }

        return data

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> dict[str, list[int | float]]: # noqa
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        dict[str, list[int | float]]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class AbsorptionVelocityDipoleExtractor(AbsorptionElectricDipoleExtractor):
    '''
    Extracts ABSORPTION SPECTRUM VIA TRANSITION VELOCITY DIPOLE MOMENTS table
    from ORCA output file for versions newer than 6.
    '''

    # Regex Start Pattern
    START_PATTERN = rb'(?<=ABSORPTION SPECTRUM VIA TRANSITION VELOCITY DIPOLE MOMENTS\s[\S\s]{408}\s)' # noqa

    # Regex End Pattern
    END_PATTERN = rb'(?=-{77})'


class OldAbsorptionElectricDipoleExtractor(extto.BetweenExtractor):
    '''
    Extracts ABSORPTION SPECTRUM VIA TRANSITION ELECTRIC DIPOLE MOMENTS table
    from ORCA output file for versions older than 6.
    '''

    # Regex Start Pattern
    START_PATTERN = rb'(?<=ABSORPTION SPECTRUM VIA TRANSITION ELECTRIC DIPOLE MOMENTS\s[\S\s]{311}\s)' # noqa

    # Regex End Pattern
    END_PATTERN = rb'(?=-{77})'

    @property
    def data(self) -> dict[str, list[int | float]]:
        '''
        Absorption spectrum data:\n
        A dictionary with keys:\n
            state\n
            energy (cm^-1)\n
            wavelength (nm)\n
            fosc\n
            t2 (a.u.^2)\n
            tx (a.u.)\n
            ty (a.u.)\n
            tz (a.u.)\n
        All values are list[float], but 'state' entries are list[int]
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> dict[str, list[int | float]]: # noqa
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        dict[str, list[int | float]]
        '''

        result = re.findall(
            r'\s+(\d+)\s+(\d*\.\d)\s+(\d*\.\d)\s+(\d\.\d{9})\s+(\d\.\d{5})\s+(-*\d\.\d{5})\s+(-*\d\.\d{5})\s+(-*\d\.\d{5})', # noqa
            block
        )

        result = np.asarray(result, dtype=float).T

        data = {
            'state': result[0].tolist(),
            'energy (cm^-1)': result[1].tolist(),
            'wavelength (nm)': result[2].tolist(),
            'fosc': result[3].tolist(),
            't2 (a.u.^2)': result[4].tolist(),
            'tx (a.u).': result[5].tolist(),
            'ty (a.u).': result[6].tolist(),
            'tz (a.u).': result[7].tolist()
        }

        data['state'] = [int(s) for s in data['state']]
        return data

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> dict[str, list[int | float]]: # noqa
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        dict[str, list[int | float]]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class OldAbsorptionVelocityDipoleExtractor(OldAbsorptionElectricDipoleExtractor): # noqa
    '''
    Extracts ABSORPTION SPECTRUM VIA TRANSITION VELOCITY DIPOLE MOMENTS table
    from ORCA output file for versions older than 6.
    '''

    # Regex Start Pattern
    START_PATTERN = rb'(?<=ABSORPTION SPECTRUM VIA TRANSITION VELOCITY DIPOLE MOMENTS\s[\S\s]{311}\s)' # noqa

    # Regex End Pattern
    END_PATTERN = rb'(?=-{77})'


class HessNameInputExtractor(extto.LineExtractor):
    '''
    Extracts Hessian file name from %mtr block of input file
    '''

    # Regex pattern for line
    PATTERN = rb' *inhessname +"[A-Za-z\._0-9]*"'

    MODIFIERS = [re.IGNORECASE]

    @property
    def data(self) -> list[str]:
        '''
        Hessian file name. One entry per match
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> str:
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        str
            Hessian file name
        '''

        name = re.findall(r'"([A-Za-z\._0-9]*)"', block)[0]

        return name

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[str]:
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[str]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class NProcsInputExtractor(extto.BetweenExtractor):
    '''
    Extracts Number of processors from input file
    '''
    # Regex Start Pattern
    START_PATTERN = rb'(?<=%PAL)'

    # Regex End Pattern
    END_PATTERN = rb'(?=END)'

    MODIFIERS = [re.IGNORECASE]

    @property
    def data(self) -> list[int]:
        '''
        Number of cores. One entry per match
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> int:
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        int
            Number of cores
        '''

        if block.count('%') > 1:
            raise DataFormattingError(
                '%pal block is malformed, perhaps missing END?'
            )

        n_cores = re.findall(r'(\d+)', block)[0]

        n_cores = int(n_cores)

        return n_cores

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[int]:
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[int]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class XYZFileInputExtractor(extto.LineExtractor):
    '''
    Extracts .xyz file name from the following line of an input file

    *xyzfile charge multiplicity file_name
    '''

    # Regex pattern for line
    PATTERN = rb'\* *xyzfile *-?\d+ *\d+ *.*'

    MODIFIERS = [re.IGNORECASE]

    @property
    def data(self) -> list[str]:
        '''
        xyz file, one entry per match
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> int:
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        str
            xyz file name
        '''

        file = block.split()[-1]

        return file

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[str]:
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[str]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class MOReadExtractor(extto.LineExtractor):
    '''
    Extracts lines containing MORead from input file

    This should (always) be the "simple input" line
    '''

    # Regex pattern for line
    PATTERN = rb'moread'

    MODIFIERS = [re.IGNORECASE]

    @property
    def data(self) -> list[str]:
        '''
        Line containing MORead, one entry per match
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> int:
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        str
            String block extracted from file
        '''

        return block

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[str]:
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[str]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class MOInpExtractor(extto.LineExtractor):
    '''
    Extracts MOInp file from input file
    '''

    # Regex pattern for line
    PATTERN = rb'% *moinp *".*"'

    MODIFIERS = [re.IGNORECASE]

    @property
    def data(self) -> list[str]:
        '''
        Line containing input orbital file, one entry per match
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> int:
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        str
            String block extracted from file
        '''

        _ext = re.compile(r'"(.*\.*.*)"')

        data = _ext.findall(block)[0]

        return data

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[str]:
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[str]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)

        return _ext.data


class XYZInputExtractor(extto.LineExtractor):
    '''
    Extracts .xyz line of an input file

    *xyz charge multiplicity
    '''

    # Regex pattern for line
    PATTERN = rb'\* *xyz *-?\d+ *\d'

    MODIFIERS = [re.IGNORECASE]

    @property
    def data(self) -> list[str]:
        '''
        *xyz line, one entry per match
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> int:
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        str
            String block extracted from file
        '''

        return block

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[str]:
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[str]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class MaxCoreInputExtractor(extto.LineExtractor):
    '''
    Extracts maxcore from input file\n
    i.e. the amount of memory allocated per core
    '''

    # Regex pattern for line
    PATTERN = rb'%maxcore +\d+'

    MODIFIERS = [re.IGNORECASE]

    @property
    def data(self) -> list[int]:
        '''
        Maxcore. One entry per match
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> int:
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        int
            Maxcore
        '''

        maxcore = re.findall(r'(\d+)', block)[0]

        maxcore = int(maxcore)

        return maxcore

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[int]:
        '''
        Convenience method which instantiates class, extracts blocks, and\n
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[int]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class SimpleInputExtractor(extto.LineExtractor):
    '''
    Extracts simple input lines (lines beginning with !)
    from ORCA input file
    '''

    # Regex pattern for line
    PATTERN = rb'^ *!.*'

    MODIFIERS = [re.IGNORECASE]

    @property
    def data(self) -> list[str]:
        '''
        Simple input lines (lines beginning with !). One entry per match
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> int:
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        str
            Simple input lines (lines beginning with !)
        '''

        return block

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[int]:
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[str]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class GMatrixExtractor(extto.BetweenExtractor):
    '''
    Extracts ELECTRONIC G-MATRIX block from output file
    '''

    # Regex Start Pattern
    START_PATTERN = rb'(?<=ELECTRONIC G-MATRIX\s[\S\s]{20}\s)'

    # Regex End Pattern
    END_PATTERN = rb'(?=Tensor is right-handed)'

    @property
    def data(self) -> dict[str, NDArray]:
        '''
        G Matrix data:\n
        A dictionary with keys:\n
            matrix\n
            values\n
            vectors
        All values are ndarray of floats
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> dict[str, NDArray]:
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        dict[str, NDArray]
        '''

        # Get Matrix
        _matrix = re.findall(
            r'(\s-?\d\.\d{6}\s+-?\d\.\d{6}\s+-?\d\.\d{6})',
            block
        )[:3]

        # Convert to matrix of floats
        matrix = np.asarray([
            [float(v) for v in val.split()]
            for val in _matrix
        ])

        # Diagonalise g.g^T
        vals, vecs = la.eigh(matrix @ matrix.T)

        # get g values as sqrt of eigenvalues of g.g^T
        vals = np.sqrt(vals)

        data = {
            'matrix': matrix,
            'values': vals,
            'vectors': vecs
        }

        return data

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[dict[str, NDArray]]: # noqa
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[dict[str, NDArray]]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class GMatrixEffectiveExtractor(GMatrixExtractor):
    '''
    Extracts ELECTRONIC G-MATRIX FROM EFFECTIVE HAMILTONIAN block from output\n
     file
    '''
    # Regex Start Pattern
    START_PATTERN = rb'(?<=ELECTRONIC G-MATRIX FROM EFFECTIVE HAMILTONIAN\s[\S\s]{46}\s)' # noqa

    @property
    def data(self) -> dict[str, NDArray | int]:
        '''
        G Matrix data:\n
        A dictionary with keys:\n
            matrix\n
            values\n
            vectors\n
            spin_mult
        All values are ndarray of floats, other than spin_mult which is int
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> dict[str, NDArray | int]:
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        dict[str, NDArray | int]
        '''

        # Get spin multiplicity
        spin_mult = int(re.findall(r'Spin multiplicity = (\d*)', block)[0])

        # Get Matrix
        _matrix = re.findall(
            r'(\s-?\d\.\d{6}\s+-?\d\.\d{6}\s+-?\d\.\d{6})',
            block
        )[:3]

        # Convert to matrix of floats
        matrix = np.asarray([
            [float(v) for v in val.split()]
            for val in _matrix
        ])

        # Diagonalise g.g^T
        vals, vecs = la.eigh(matrix @ matrix.T)

        # get g values as sqrt of eigenvalues of g.g^T
        vals = np.sqrt(vals)

        data = {
            'matrix': matrix,
            'values': vals,
            'vectors': vecs,
            'spin_mult': spin_mult
        }

        return data

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[dict[str, NDArray | int]]: # noqa
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[dict[str, NDArray | int]]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class GMatrixLExtractor(GMatrixExtractor):
    '''
    Extracts ELECTRONIC G-MATRIX: L contribution block from output file
    '''
    # Regex Start Pattern
    START_PATTERN = rb'(?<=ELECTRONIC G-MATRIX: L contribution\s[\S\s]{47}\s)'


class GMatrixSExtractor(GMatrixExtractor):
    '''
    Extracts ELECTRONIC G-MATRIX: S contribution block from output file
    '''
    # Regex Start Pattern
    START_PATTERN = rb'(?<=ELECTRONIC G-MATRIX: S contribution\s[\S\s]{47}\s)'


class SpinFreeEnergyExtractor(extto.BetweenExtractor):
    '''
    Extracts Spin-Free TRANSITION ENERGIES block from output file
    '''

    # Regex Start Pattern
    START_PATTERN = rb'(?<= TRANSITION ENERGIES\s[\S\s]{30}\s)'

    # Regex End Pattern
    END_PATTERN = rb'(?=----)'

    @property
    def data(self) -> dict[str, NDArray]:
        '''
        State Energies:\n
        A dictionary with keys:\n
            root\n
            multiplicity\n
            energy (a.u.)\n
            delta energy (cm^-1)\n
        All values are ndarray of floats
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> dict[str, NDArray]:
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        dict[str, NDArray]
        '''

        # Get energy, root, and mult of lowest energy state
        s0e = float(re.findall(r'MULT \d*\) = \s+(-\d+\.\d+)', block)[0])
        s0r = int(re.findall(r'\(ROOT (\d*)', block)[0])
        s0m = int(re.findall(r'MULT (\d*)', block)[0])

        # Extract ROOT MULT and DE in atomic units
        result = re.findall(
            r'\s+(\d+)\s+(\d+)\s+(\d+\.\d{6})',
            block
        )
        result = np.asarray(result, dtype=str).T

        roots = [s0r] + result[0].astype(int).tolist()
        mults = [s0m] + result[1].astype(int).tolist()
        # Absolute energies in hartree
        energies = result[2].astype(float) + s0e
        energies = [s0e] + energies.tolist()

        # Calculate delta energies in cm-1
        denergies = result[2].astype(float) * 219474.63
        denergies = denergies.tolist()

        data = {
            'root': roots,
            'multiplicity': mults,
            'energy (a.u.)': energies,
            'delta energy (cm^-1)': denergies
        }

        return data

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[dict[str, NDArray]]: # noqa
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[dict[str, NDArray]]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data


class SpinOrbitEnergyExtractor(extto.BetweenExtractor):
    '''
    Extracts Spin-Orbit Energies block from output file
    '''

    # Regex Start Pattern
    START_PATTERN = rb'(?<=Lowest eigenvalue of the SOC matrix:)'

    # Regex End Pattern
    END_PATTERN = rb'(?=The threshold for printing is)'

    @property
    def data(self) -> NDArray:
        '''
        Difference in state energies relative to first state (0 Eh) as\n
        NDArray of floats
        '''
        return self._data

    @staticmethod
    def _process_block(block: str) -> NDArray:
        '''
        Converts single block into data entries described in self.data

        Parameters
        ----------
        block: str
            String block extracted from file

        Returns
        -------
        NDArray
        '''

        # Extract energies from table
        result = re.findall(
            r'\s+\d+:\s+(\d+\.\d{2})\s+\d+\.\d{4}\s+\d\.\d{2}e-+\d{2}',
            block
        )
        result = np.asarray(result, dtype=str).T.astype(float)

        return result

    @classmethod
    def extract(cls, file_name: str | pathlib.Path) -> list[dict[str, NDArray]]: # noqa
        '''
        Convenience method which instantiates class, extracts blocks, and
        returns processed datasets

        Parameters
        ----------
        file_name: str | pathlib.Path
            File to parse

        Returns
        -------
        list[dict[str, NDArray]]
            Each entry contains processed data, as defined in cls.data
        '''
        _ext = cls()
        _ext(file_name, process=True)
        return _ext.data
