"""Force-field read/write helpers for LAMMPS-style inputs."""

from __future__ import annotations

import os
import warnings
from pathlib import Path
from typing import Any, Dict, List, Optional, Sequence, Tuple

import numpy as np

from ..fitters import TABLE_FITTERS
from ..potentials import POTENTIAL_REGISTRY
from ..potentials.base import BasePotential, potential_style_labels
from ..topology.forcefield import Forcefield
from ..topology.topology_array import TopologyArrays
from ..topology.types import InteractionKey
from .lammps_input import iter_commands, tokenize_line
from .tables import (
    cap_table_forces,
    estimate_table_fp,
    find_equilibrium,
    parse_lammps_table,
    write_lammps_table,
)
_BOUND_SECTION_TOKENS = frozenset({"lb", "ub"})


def _parse_bool_mask_tokens(tokens: Sequence[str]) -> Optional[np.ndarray]:
    from ..configs.utils import parse_bool_token

    values: list[bool] = []
    for token in tokens:
        parsed = parse_bool_token(token)
        if parsed is None:
            return None
        values.append(parsed)
    return np.asarray(values, dtype=bool)


def _parse_pair_mask_spec(
    tokens: Sequence[str],
    *,
    pair_style: str,
    pair_typ_sel: Optional[Sequence[str]],
) -> Optional[tuple[InteractionKey, Optional[str], list[str]]]:
    if len(tokens) < 3 or tokens[0] != "pair_coeff":
        return None
    style = str(pair_style).strip().lower()
    potential_style: Optional[str] = None
    payload = [str(token) for token in tokens[3:]]
    if style == "hybrid":
        if len(tokens) < 4:
            return None
        style = str(tokens[3]).strip().lower()
        potential_style = style
        payload = [str(token) for token in tokens[4:]]
    elif payload and payload[0].strip().lower() == style:
        potential_style = style
        payload = payload[1:]

    if pair_typ_sel is not None:
        allowed = {str(token).strip().lower() for token in pair_typ_sel}
        if style not in allowed:
            return None

    return InteractionKey.pair(tokens[1], tokens[2]), potential_style, payload


def _parse_bonded_mask_spec(
    tokens: Sequence[str],
    *,
    topology_arrays: TopologyArrays,
) -> Optional[tuple[InteractionKey, Optional[str], list[str]]]:
    if len(tokens) < 3 or tokens[0] not in {"bond_coeff", "angle_coeff"}:
        return None
    kind = tokens[0].split("_", 1)[0]
    potential_style = str(tokens[2]).strip().lower()
    type_num = str(tokens[1])
    key = InteractionKey(style=kind, types=(type_num,))
    # Match ReadLmpFF: LAMMPS bonded type ids are 1-based in coeff files,
    # while TopologyArrays stores AceCG internal bonded type maps 0-based
    # after applying any configured type aliases.
    if type_num.isdigit():
        bonded_type_id_to_key = getattr(topology_arrays, f"{kind}_type_id_to_key", None)
        if bonded_type_id_to_key is None:
            raise ValueError(
                f"topology_arrays has no {kind}_type_id_to_key map required "
                f"for numeric {tokens[0]} entries."
            )
        mapped_key = bonded_type_id_to_key.get(int(type_num) - 1)
        if mapped_key is None:
            raise KeyError(
                f"{tokens[0]} type id {type_num} does not exist in topology_arrays."
            )
        key = mapped_key
    return key, potential_style, [str(token) for token in tokens[3:]]


def _looks_like_mask_payload(payload_tokens: Sequence[str]) -> bool:
    payload = [str(token) for token in payload_tokens]
    if not payload:
        return True
    if payload[0].strip().lower() in {"mask", "unmask"}:
        return True
    return _parse_bool_mask_tokens(payload) is not None


def _parse_bounds_payload(
    payload_tokens: Sequence[str],
) -> Optional[tuple[tuple[str, ...], tuple[str, ...]]]:
    payload = [str(token) for token in payload_tokens]
    if not payload:
        return None

    values: dict[str, list[str]] = {"lb": [], "ub": []}
    seen: set[str] = set()
    current: Optional[str] = None
    for token in payload:
        lowered = token.strip().lower()
        if lowered in _BOUND_SECTION_TOKENS:
            if lowered in seen:
                raise ValueError(f"Duplicate bounds section {lowered!r}.")
            seen.add(lowered)
            current = lowered
            continue
        if current is None:
            return None
        values[current].append(token)

    if not seen:
        return None
    return tuple(values["lb"]), tuple(values["ub"])


def _read_lmpffmask_spec(
    file: str,
    pair_style: str,
    *,
    topology_arrays: TopologyArrays,
    pair_typ_sel: Optional[Sequence[str]] = None,
) -> tuple[tuple[InteractionKey, Optional[str], tuple[str, ...]], ...]:
    entries: list[tuple[InteractionKey, Optional[str], tuple[str, ...]]] = []
    seen: set[tuple[InteractionKey, Optional[str]]] = set()

    for tokens in iter_commands(file):
        parsed = _parse_pair_mask_spec(
            tokens,
            pair_style=pair_style,
            pair_typ_sel=pair_typ_sel,
        )
        if parsed is None:
            parsed = _parse_bonded_mask_spec(tokens, topology_arrays=topology_arrays)
        if parsed is None:
            continue

        key, potential_style, payload = parsed
        if not _looks_like_mask_payload(payload):
            continue
        seen_key = (key, potential_style)
        if seen_key in seen:
            style_label = f" {potential_style}" if potential_style else ""
            raise ValueError(f"Duplicate mask entry for {key.label()}{style_label} in {file!r}.")
        seen.add(seen_key)
        entries.append((key, potential_style, tuple(payload)))

    return tuple(entries)


def _read_lmpffbounds_spec(
    file: str,
    pair_style: str,
    *,
    topology_arrays: TopologyArrays,
    pair_typ_sel: Optional[Sequence[str]] = None,
) -> tuple[tuple[InteractionKey, Optional[str], tuple[str, ...], tuple[str, ...]], ...]:
    entries: list[tuple[InteractionKey, Optional[str], tuple[str, ...], tuple[str, ...]]] = []
    seen: set[tuple[InteractionKey, Optional[str]]] = set()

    for tokens in iter_commands(file):
        parsed = _parse_pair_mask_spec(
            tokens,
            pair_style=pair_style,
            pair_typ_sel=pair_typ_sel,
        )
        if parsed is None:
            parsed = _parse_bonded_mask_spec(tokens, topology_arrays=topology_arrays)
        if parsed is None:
            continue

        key, potential_style, payload = parsed
        bounds_payload = _parse_bounds_payload(payload)
        if bounds_payload is None:
            continue
        seen_key = (key, potential_style)
        if seen_key in seen:
            style_label = f" {potential_style}" if potential_style else ""
            raise ValueError(f"Duplicate bounds entry for {key.label()}{style_label} in {file!r}.")
        seen.add(seen_key)
        lb_tokens, ub_tokens = bounds_payload
        entries.append((key, potential_style, lb_tokens, ub_tokens))

    return tuple(entries)


def ReadLmpFFMask(
    file: str,
    pair_style: str,
    *,
    topology_arrays: TopologyArrays,
    pair_typ_sel: Optional[List[str]] = None,
) -> Any:
    """Read an AceCG forcefield-mask file.

    Parameters
    ----------
    file : str
        Path to the mask file.
    pair_style : str
        LAMMPS pair style in the source forcefield file. Use ``"hybrid"`` for
        hybrid pair coefficient lines.
    topology_arrays : TopologyArrays
        Topology metadata used to translate numeric bonded type ids into
        canonical :class:`InteractionKey` values.
    pair_typ_sel : list[str], optional
        Pair sub-styles to include when ``pair_style="hybrid"``.

    Returns
    -------
    ForcefieldMaskSpec
        Parsed mask entries grouped by :class:`InteractionKey`.
    """
    from ..configs.models import ForcefieldMaskSpec

    return ForcefieldMaskSpec(entries=_read_lmpffmask_spec(
        file,
        str(pair_style),
        pair_typ_sel=pair_typ_sel,
        topology_arrays=topology_arrays,
    ))


def ReadLmpFFBounds(
    file: str,
    pair_style: str,
    *,
    topology_arrays: TopologyArrays,
    pair_typ_sel: Optional[List[str]] = None,
) -> Any:
    """Read an AceCG forcefield-bounds file.

    Bounds entries use ``lb`` and/or ``ub`` sections after the optional
    potential style, for example ``pair_coeff A B gauss/cut lb None 5 0.3 ub
    -0.1 15 None``. ``None`` leaves that side unbounded for one parameter.
    Numeric bonded coefficient entries are resolved through ``topology_arrays``
    so their LAMMPS type ids map to canonical :class:`InteractionKey` values.
    """
    from ..configs.models import ForcefieldBoundsSpec

    return ForcefieldBoundsSpec(entries=_read_lmpffbounds_spec(
        file,
        str(pair_style),
        pair_typ_sel=pair_typ_sel,
        topology_arrays=topology_arrays,
    ))


def _set_lmpff_potential_style(pot: BasePotential, style: str) -> BasePotential:
    """Attach the source LAMMPS style used by mask selectors."""
    setattr(pot, "_acecg_lammps_style", str(style).strip().lower())
    return pot


def _append_lmpff_potential(
    forcefield: Dict[InteractionKey, List[BasePotential]],
    key: InteractionKey,
    pot: BasePotential,
    style: str,
) -> None:
    """Append a parsed potential, preserving overlay stacks for one key."""
    forcefield.setdefault(key, []).append(_set_lmpff_potential_style(pot, style))


def _select_lmpff_potential(
    value: BasePotential | List[BasePotential],
    style: str,
    key: InteractionKey,
    occurrence: Optional[int] = None,
) -> BasePotential:
    pots = value if isinstance(value, list) else [value]
    if len(pots) == 1:
        if occurrence is None or occurrence == 0:
            return pots[0]
        raise KeyError(
            f"No occurrence {occurrence + 1} of style {style!r} found for {key.label()}. "
            "The forcefield has only one potential for this interaction."
        )
    style_norm = str(style).strip().lower()
    matches = [pot for pot in pots if style_norm in potential_style_labels(pot)]
    if not matches:
        available = sorted(
            {
                label
                for pot in pots
                for label in potential_style_labels(pot)
            }
        )
        raise KeyError(
            f"No potential with style {style!r} found for {key.label()}. "
            f"Available styles: {available}"
        )
    if occurrence is not None:
        if occurrence < len(matches):
            return matches[occurrence]
        raise KeyError(
            f"No occurrence {occurrence + 1} of style {style!r} found for {key.label()}. "
            f"The template asks for at least {occurrence + 1}, but the forcefield has {len(matches)}."
        )
    if len(matches) == 1:
        return matches[0]
    raise KeyError(
        f"Multiple potentials with style {style!r} found for {key.label()}; "
        "style-qualified LAMMPS writing is ambiguous."
    )


def ReadLmpFF(
    file: str,
    pair_style: str,
    pair_typ_sel: Optional[List[str]] = None,
    cutoff: Optional[float] = None,
    global_var: Optional[dict] = None,
    table_fit: str = "multigaussian",
    table_fit_overrides: Optional[dict] = None,
    topology_arrays: Optional[Any] = None,
):
    """Read a LAMMPS-style forcefield file.

    Parameters
    ----------
    file : str
        Input LAMMPS forcefield/include file containing ``*_coeff`` lines.
    pair_style : str
        Active pair style, or ``"hybrid"`` when pair coefficient lines include
        per-pair sub-style names.
    pair_typ_sel : list[str], optional
        Hybrid sub-styles to read. Ignored for non-hybrid pair styles.
    cutoff : float, optional
        Cutoff appended to pair potential constructors when the coefficient
        line does not carry it explicitly.
    global_var : dict, optional
        Global variables required by some LAMMPS styles, such as
        ``lj/cut/soft``.
    table_fit : str, default="multigaussian"
        Registered table fitter used when reading ``table`` coefficients.
    table_fit_overrides : dict, optional
        Extra keyword arguments passed to the table fitter.
    topology_arrays : object, optional
        Topology metadata used to translate numeric bonded/pair type ids into
        canonical :class:`InteractionKey` values.

    Returns
    -------
    Forcefield
        Unified AceCG forcefield container.
    """
    assert pair_style is not None
    if pair_style != "hybrid":
        pair_typ_sel = None
        param_offset = 3
    else:
        param_offset = 4
    if topology_arrays is None:
        warnings.warn(
            "ReadLmpFF called without topology_arrays; bonded InteractionKeys will be "
            "constructed directly from bonded type ids.",
            stacklevel=2,
        )

    base_dir = os.path.dirname(os.path.abspath(file))
    forcefield: Dict[InteractionKey, List[BasePotential]] = {}

    for tmp in iter_commands(file):
        keyword = tmp[0]
        if keyword == "pair_coeff":
            if len(tmp) <= param_offset:
                continue
            if pair_style == "hybrid":
                style = tmp[3]
            else:
                style = pair_style
            typ1, typ2 = tmp[1], tmp[2]
            pair_typ1 = typ1
            pair_typ2 = typ2
            atom_type_code_to_name = getattr(topology_arrays, "atom_type_code_to_name", None)
            if atom_type_code_to_name is not None and str(typ1).isdigit() and str(typ2).isdigit():
                pair_typ1 = str(atom_type_code_to_name.get(int(typ1), typ1))
                pair_typ2 = str(atom_type_code_to_name.get(int(typ2), typ2))
            pair = InteractionKey.pair(str(pair_typ1), str(pair_typ2))

            if pair_typ_sel is None or style in pair_typ_sel:
                if style == "table":
                    table_file = tmp[param_offset]
                    if not os.path.isabs(table_file):
                        table_file = os.path.join(base_dir, table_file)
                    fitter_overrides = dict(table_fit_overrides or {})
                    if table_fit == "bspline":
                        fitter_overrides["bonded"] = False
                    fitter = TABLE_FITTERS.create(table_fit, **fitter_overrides)
                    pot = fitter.fit(table_file, typ1=pair.types[0], typ2=pair.types[1])
                    _append_lmpff_potential(forcefield, pair, pot, style)
                elif style in POTENTIAL_REGISTRY:
                    constructor = POTENTIAL_REGISTRY[style]
                    params = list(map(float, tmp[param_offset:]))
                    if style == "double/gauss":
                        if cutoff is None:
                            gauss_params, cutoff = params[:-1], params[-1]
                        else:
                            gauss_params = params[:]
                        pot = constructor(pair.types[0], pair.types[1], 2, cutoff, gauss_params)
                    elif style == "lj/cut/soft":
                        if cutoff is not None:
                            params.append(cutoff)
                        if global_var is None:
                            raise ValueError("global_var is required for lj/cut/soft force fields")
                        params.append(global_var["n"])
                        params.append(global_var["alpha"])
                        pot = constructor(pair.types[0], pair.types[1], *params)
                    else:
                        if cutoff is not None:
                            params.append(cutoff)
                        pot = constructor(pair.types[0], pair.types[1], *params)
                    _append_lmpff_potential(forcefield, pair, pot, style)

        elif keyword in {"bond_coeff", "angle_coeff"}:
            if len(tmp) < 4:
                continue
            kind = keyword.split("_", 1)[0]
            type_num = tmp[1]
            bonded_style = tmp[2]
            bonded_type_id_to_key = getattr(topology_arrays, f"{kind}_type_id_to_key", None)
            key = InteractionKey(style=kind, types=(type_num,))
            if bonded_type_id_to_key is not None and str(type_num).isdigit():
                key = bonded_type_id_to_key.get(int(type_num) - 1, key)

            if bonded_style == "table" and len(tmp) >= 5:
                table_file = tmp[3]
                if not os.path.isabs(table_file):
                    table_file = os.path.join(base_dir, table_file)
                fitter_overrides = dict(table_fit_overrides or {})
                if table_fit == "bspline":
                    fitter_overrides["bonded"] = True
                    # Bonded tables often rely on their outer padding as a
                    # restoring tail; do not flatten them at the table edge.
                    fitter_overrides["anchor_to_cutoff"] = False
                fitter = TABLE_FITTERS.create(table_fit, **fitter_overrides)
                pot = fitter.fit(table_file, typ1=key.types[0], typ2=key.types[-1])
                forcefield[key] = [_set_lmpff_potential_style(pot, bonded_style)]

            elif bonded_style == "harmonic" and len(tmp) >= 5:
                from ..potentials.harmonic import HarmonicPotential

                k_val = float(tmp[3])
                r0_val = float(tmp[4])
                scale = (np.pi / 180.0) ** 2 if kind == "angle" else 1.0
                typ1 = key.types[0]
                typ2 = key.types[-1]
                typ3 = key.types[1] if kind == "angle" and len(key.types) == 3 else None
                pot = HarmonicPotential(typ1, typ2, k_val, r0_val, typ3=typ3, scale=scale)
                forcefield[key] = [_set_lmpff_potential_style(pot, bonded_style)]

    return Forcefield(forcefield)


def WriteLmpFF(
    old_file: str,
    new_file: str,
    forcefield: Forcefield,
    pair_style: str,
    pair_typ_sel: Optional[List[str]] = None,
    topology_arrays: Optional[Any] = None,
):
    """Write forcefield parameters into a LAMMPS-style coefficient file.

    Parameters
    ----------
    old_file : str
        Source coefficient/include file whose structure is preserved.
    new_file : str
        Destination file path.
    forcefield : Forcefield
        Forcefield containing updated parameters.
    pair_style : str
        Active pair style, or ``"hybrid"`` for hybrid coefficient lines.
    pair_typ_sel : list[str], optional
        Hybrid sub-styles to write. Ignored for non-hybrid pair styles.
    topology_arrays : object, optional
        Topology metadata used to translate numeric type ids into canonical
        :class:`InteractionKey` values.
    """
    assert pair_style is not None
    if pair_style != "hybrid":
        pair_typ_sel = None
        param_offset = 3
    else:
        param_offset = 4
    if topology_arrays is None:
        warnings.warn(
            "WriteLmpFF called without topology_arrays; bonded InteractionKeys will be "
            "matched directly against bonded type ids.",
            stacklevel=2,
        )

    old_base_dir = os.path.dirname(os.path.abspath(old_file))
    new_base_dir = os.path.dirname(os.path.abspath(new_file))
    with open(old_file, "r", encoding="utf-8") as f:
        lines = f.readlines()

    pair_occurrences: Dict[Tuple[InteractionKey, str], int] = {}
    for i, line in enumerate(lines):
        tmp = list(tokenize_line(line))
        if not tmp:
            continue
        keyword = tmp[0]
        if keyword == "pair_coeff":
            if len(tmp) <= param_offset:
                continue
            if pair_style == "hybrid":
                style = tmp[3]
            else:
                style = pair_style
            lookup_key = InteractionKey.pair(tmp[1], tmp[2])
            atom_type_code_to_name = getattr(topology_arrays, "atom_type_code_to_name", None)
            if atom_type_code_to_name is not None and tmp[1].isdigit() and tmp[2].isdigit():
                lookup_key = InteractionKey.pair(
                    str(atom_type_code_to_name.get(int(tmp[1]), tmp[1])),
                    str(atom_type_code_to_name.get(int(tmp[2]), tmp[2])),
                )

            if pair_typ_sel is None or style in pair_typ_sel:
                if lookup_key in forcefield:
                    occurrence_key = (lookup_key, str(style).strip().lower())
                    occurrence = pair_occurrences.get(occurrence_key, 0)
                    pair_occurrences[occurrence_key] = occurrence + 1
                    pot = _select_lmpff_potential(
                        forcefield[lookup_key],
                        style,
                        lookup_key,
                        occurrence=occurrence,
                    )
                    if style == "table":
                        if len(tmp) <= param_offset + 1:
                            continue
                        table_token = tmp[param_offset]
                        source_table_file = table_token
                        dest_table_file = table_token
                        if not os.path.isabs(table_token):
                            source_table_file = os.path.join(old_base_dir, table_token)
                            dest_table_file = os.path.join(new_base_dir, table_token)

                        Path(dest_table_file).parent.mkdir(parents=True, exist_ok=True)
                        r, _, _ = parse_lammps_table(source_table_file)
                        write_lammps_table(
                            filename=dest_table_file,
                            r=r,
                            V=pot.value(r),
                            F=pot.force(r),
                            comment=f"Table {table_token}: id, r, potential, force",
                            table_name=tmp[param_offset + 1],
                            table_style="pair",
                        )
                        max_force = getattr(pot, "_acecg_max_force", None)
                        if max_force is not None:
                            cap_table_forces(dest_table_file, max_force=max_force)
                    else:
                        params = pot.lammps_params() if hasattr(pot, "lammps_params") else pot.get_params()
                        n_param = len(params)
                        tmp[param_offset : param_offset + n_param] = map(str, params)
                        lines[i] = "   ".join(tmp) + "\n"

        elif keyword in {"bond_coeff", "angle_coeff"}:
            if len(tmp) < 3:
                continue
            kind = keyword.split("_", 1)[0]
            type_num = tmp[1]
            lookup_key = InteractionKey(style=kind, types=(type_num,))
            bonded_type_id_to_key = getattr(topology_arrays, f"{kind}_type_id_to_key", None)
            if bonded_type_id_to_key is not None and type_num.isdigit():
                lookup_key = bonded_type_id_to_key.get(int(type_num) - 1, lookup_key)
            if lookup_key not in forcefield:
                continue
            bonded_style = tmp[2]
            pot = _select_lmpff_potential(forcefield[lookup_key], bonded_style, lookup_key)
            if bonded_style == "table":
                if len(tmp) < 4:
                    continue
                table_token = tmp[3]
                source_table_file = table_token
                dest_table_file = table_token
                if not os.path.isabs(table_token):
                    source_table_file = os.path.join(old_base_dir, table_token)
                    dest_table_file = os.path.join(new_base_dir, table_token)
                Path(dest_table_file).parent.mkdir(parents=True, exist_ok=True)
                r, _, _ = parse_lammps_table(source_table_file)
                V = pot.value(r)
                F = pot.force(r)
                table_name = tmp[4] if len(tmp) > 4 else Path(dest_table_file).stem
                eq = find_equilibrium(r, F)
                fp = estimate_table_fp(r, F)
                write_lammps_table(
                    filename=dest_table_file,
                    r=r,
                    V=V,
                    F=F,
                    comment=f"AceCG updated {kind} {table_name}",
                    table_name=table_name,
                    table_style=kind,
                    eq=eq,
                    fp=fp,
                )
            elif bonded_style == "harmonic":
                params = list(pot.lammps_params() if hasattr(pot, "lammps_params") else pot.get_params())
                tmp[3:] = [f"{p:.8g}" for p in params]
                lines[i] = "   ".join(tmp) + "\n"

    Path(new_file).parent.mkdir(parents=True, exist_ok=True)
    with open(new_file, "w", encoding="utf-8") as f:
        f.writelines(lines)


def resolve_source_table_entries(
    source_forcefield_path: str,
    *,
    pair_style: str = "table",
) -> Dict[InteractionKey, Dict[str, str]]:
    """Parse a LAMMPS settings file to extract table-style interaction entries.

    Returns a mapping from ``InteractionKey`` to a dict with keys
    ``table_path``, ``table_name``, ``style``.
    """
    settings_path = Path(source_forcefield_path).resolve()
    entries: Dict[InteractionKey, Dict[str, str]] = {}

    for tokens in iter_commands(settings_path):
        keyword = tokens[0].lower()
        if keyword == "pair_coeff":
            if pair_style == "hybrid":
                if len(tokens) < 6:
                    continue
                source_style = tokens[3].lower()
                table_token = tokens[4]
                table_name = tokens[5]
            else:
                if len(tokens) < 5:
                    continue
                source_style = pair_style.lower()
                table_token = tokens[3]
                table_name = tokens[4]
            if source_style != "table":
                continue
            key = InteractionKey.pair(tokens[1], tokens[2])
            table_path = _resolve_table_path(settings_path, table_token)
            entries[key] = {
                "table_path": str(table_path),
                "table_name": str(table_name),
                "style": "pair",
            }
            continue

        if keyword in {"bond_coeff", "angle_coeff"}:
            if len(tokens) < 5:
                continue
            kind = keyword.split("_", 1)[0]
            source_style = tokens[2].lower()
            if source_style != "table":
                continue
            key = InteractionKey(style=kind, types=(str(tokens[1]),))
            table_path = _resolve_table_path(settings_path, tokens[3])
            table_name = (
                tokens[4] if len(tokens) > 4 else Path(str(tokens[3])).stem
            )
            entries[key] = {
                "table_path": str(table_path),
                "table_name": str(table_name),
                "style": kind,
            }

    return entries


def _resolve_table_path(settings_path: Path, table_token: str) -> Path:
    token_path = Path(table_token)
    if token_path.is_absolute():
        return token_path.resolve()
    return (settings_path.parent / token_path).resolve()


__all__ = [
    "ReadLmpFF",
    "ReadLmpFFBounds",
    "ReadLmpFFMask",
    "WriteLmpFF",
    "resolve_source_table_entries",
]
