"""
mf module.  Contains the ModflowGlobal, ModflowList, and Modflow classes.


"""

import os
import warnings
from inspect import getfullargspec
from os import PathLike, curdir
from pathlib import Path
from typing import Optional, Union

import flopy

from ..discretization.modeltime import ModelTime
from ..discretization.structuredgrid import StructuredGrid
from ..discretization.unstructuredgrid import UnstructuredGrid
from ..mbase import BaseModel
from ..pakbase import Package
from ..utils import mfreadnam
from .mfpar import ModflowPar


class ModflowGlobal(Package):
    """
    ModflowGlobal Package class

    """

    def __init__(self, model, extension="glo"):
        super().__init__(model, extension, "GLOBAL", 1)
        return

    def __repr__(self):
        return "Global Package class"

    def write_file(self):
        raise NotImplementedError


class ModflowList(Package):
    """
    ModflowList Package class

    """

    def __init__(self, model, extension="list", unitnumber=2):
        super().__init__(model, extension, "LIST", unitnumber)
        return

    def __repr__(self):
        return "List Package class"

    def write_file(self):
        raise NotImplementedError


class Modflow(BaseModel):
    """
    MODFLOW Model Class.

    Parameters
    ----------
    modelname : str, default "modflowtest"
        Name of model.  This string will be used to name the MODFLOW input
        that are created with write_model.
    namefile_ext : str, default "nam"
        Extension for the namefile.
    version : str, default "mf2005"
        MODFLOW version. Choose one of: "mf2k", "mf2005" (default),
        "mfnwt", or "mfusg".
    exe_name : str or PathLike, default "mf2005"
        The name or path of the executable to use.
    structured : bool, default True
        Specify if model grid is structured (default) or unstructured.
    listunit : int, default 2
        Unit number for the list file.
    model_ws : str or PathLike, default "." (curdir)
        Model workspace.  Directory name to create model data sets.
    external_path : str or PathLike, optional
        Location for external files.
    verbose : bool, default False
        Print additional information to the screen.
    extra_pkgs : dict, optional
        Add custom packages classes to mfnam_packages. Allows for loading models
        with custom packages not contained in the standard flopy distribution.

    Attributes
    ----------

    Methods
    -------

    See Also
    --------

    Notes
    -----

    Examples
    --------
    >>> import flopy
    >>> m = flopy.modflow.Modflow()

    """

    def __init__(
        self,
        modelname="modflowtest",
        namefile_ext="nam",
        version="mf2005",
        exe_name: Union[str, PathLike] = "mf2005",
        structured=True,
        listunit=2,
        model_ws: Union[str, PathLike] = curdir,
        external_path: Union[str, PathLike, None] = None,
        verbose=False,
        extra_pkgs: Optional[dict] = None,
        **kwargs,
    ):
        super().__init__(
            modelname,
            namefile_ext,
            exe_name,
            model_ws,
            structured=structured,
            verbose=verbose,
            **kwargs,
        )
        self.version_types = {
            "mf2k": "MODFLOW-2000",
            "mf2005": "MODFLOW-2005",
            "mfnwt": "MODFLOW-NWT",
            "mfusg": "MODFLOW-USG",
        }

        self.set_version(version)

        if self.version == "mf2k":
            self.glo = ModflowGlobal(self)

        self.lst = ModflowList(self, unitnumber=listunit)
        # -- check if unstructured is specified for something
        # other than mfusg is specified
        if not self.structured:
            assert "mfusg" in self.version, (
                "structured=False can only be specified for mfusg models"
            )

        # external option stuff
        self.array_free_format = True
        self.array_format = "modflow"

        self.load_fail = False
        # the starting external data unit number
        self._next_ext_unit = 1000

        if external_path is not None:
            if os.path.exists(os.path.join(model_ws, external_path)):
                print(f"Note: external_path {external_path} already exists")
            else:
                os.makedirs(os.path.join(model_ws, external_path))
            self.external_path = str(external_path)
        else:
            self.external_path = None
        self.verbose = verbose
        self.mfpar = ModflowPar()

        # output file info
        self.hext = "hds"
        self.dext = "ddn"
        self.cext = "cbc"
        self.hpth = None
        self.dpath = None
        self.cpath = None

        # Create a dictionary to map package with package object.
        # This is used for loading models.
        self.mfnam_packages = {
            "zone": flopy.modflow.ModflowZon,
            "mult": flopy.modflow.ModflowMlt,
            "ag": flopy.modflow.ModflowAg,
            "pval": flopy.modflow.ModflowPval,
            "bas6": flopy.modflow.ModflowBas,
            "dis": flopy.modflow.ModflowDis,
            "bcf6": flopy.modflow.ModflowBcf,
            "lpf": flopy.modflow.ModflowLpf,
            "hfb6": flopy.modflow.ModflowHfb,
            "chd": flopy.modflow.ModflowChd,
            "fhb": flopy.modflow.ModflowFhb,
            "wel": flopy.modflow.ModflowWel,
            "mnw1": flopy.modflow.ModflowMnw1,
            "mnw2": flopy.modflow.ModflowMnw2,
            "mnwi": flopy.modflow.ModflowMnwi,
            "drn": flopy.modflow.ModflowDrn,
            "drt": flopy.modflow.ModflowDrt,
            "rch": flopy.modflow.ModflowRch,
            "evt": flopy.modflow.ModflowEvt,
            "ghb": flopy.modflow.ModflowGhb,
            "gmg": flopy.modflow.ModflowGmg,
            "lmt6": flopy.modflow.ModflowLmt,
            "lmt7": flopy.modflow.ModflowLmt,
            "riv": flopy.modflow.ModflowRiv,
            "str": flopy.modflow.ModflowStr,
            "swi2": flopy.modflow.ModflowSwi2,
            "pcg": flopy.modflow.ModflowPcg,
            "pcgn": flopy.modflow.ModflowPcgn,
            "nwt": flopy.modflow.ModflowNwt,
            "pks": flopy.modflow.ModflowPks,
            "sfr": flopy.modflow.ModflowSfr2,
            "lak": flopy.modflow.ModflowLak,
            "gage": flopy.modflow.ModflowGage,
            "sip": flopy.modflow.ModflowSip,
            "sor": flopy.modflow.ModflowSor,
            "de4": flopy.modflow.ModflowDe4,
            "oc": flopy.modflow.ModflowOc,
            "uzf": flopy.modflow.ModflowUzf1,
            "upw": flopy.modflow.ModflowUpw,
            "sub": flopy.modflow.ModflowSub,
            "swt": flopy.modflow.ModflowSwt,
            "hyd": flopy.modflow.ModflowHyd,
            "hob": flopy.modflow.ModflowHob,
            "chob": flopy.modflow.ModflowFlwob,
            "gbob": flopy.modflow.ModflowFlwob,
            "drob": flopy.modflow.ModflowFlwob,
            "rvob": flopy.modflow.ModflowFlwob,
            "vdf": flopy.seawat.SeawatVdf,
            "vsc": flopy.seawat.SeawatVsc,
        }
        if extra_pkgs:
            self.mfnam_packages.update(extra_pkgs)

    def __repr__(self):
        nrow, ncol, nlay, nper = self.get_nrow_ncol_nlay_nper()
        # structured case
        s = "MODFLOW {} layer(s) {} row(s) {} column(s) {} stress period(s)".format(
            nlay, nrow, ncol, nper
        )
        return s

    @property
    def modeltime(self):
        if self.get_package("disu") is not None:
            dis = self.disu
        else:
            dis = self.dis

        self._model_time = ModelTime(
            perlen=dis.perlen.array,
            nstp=dis.nstp.array,
            tsmult=dis.tsmult.array,
            time_units=dis.itmuni,
            start_datetime=dis.start_datetime,
            steady_state=dis.steady.array,
        )
        return self._model_time

    @property
    def modelgrid(self):
        if not self._mg_resync:
            return self._modelgrid

        if self.has_package("bas6"):
            ibound = self.bas6.ibound.array
        else:
            ibound = None
        # take the first non-None entry
        crs = self._modelgrid.crs or self._modelgrid.proj4 or self._modelgrid.epsg
        common_kwargs = {
            "crs": crs,
            "xoff": self._modelgrid.xoffset,
            "yoff": self._modelgrid.yoffset,
            "angrot": self._modelgrid.angrot,
        }
        if self.get_package("disu") is not None:
            # build unstructured grid
            self._modelgrid = UnstructuredGrid(
                grid_type="unstructured",
                vertices=self._modelgrid.vertices,
                ivert=self._modelgrid.iverts,
                xcenters=self._modelgrid.xcenters,
                ycenters=self._modelgrid.ycenters,
                ncpl=self.disu.nodelay.array,
                top=self.disu.top.array,
                botm=self.disu.bot.array,
                idomain=ibound,
                lenuni=self.disu.lenuni,
                iac=self.disu.iac.array,
                ja=self.disu.ja.array,
                **common_kwargs,
            )
            print("WARNING: Model grid functionality limited for unstructured grid.")
        else:
            # build structured grid
            self._modelgrid = StructuredGrid(
                self.dis.delc.array,
                self.dis.delr.array,
                self.dis.top.array,
                self.dis.botm.array,
                ibound,
                self.dis.lenuni,
                nlay=self.dis.nlay,
                laycbd=self.dis.laycbd.array,
                **common_kwargs,
            )

        # resolve offsets
        xoff = self._modelgrid.xoffset
        if xoff is None:
            if self._xul is not None:
                xoff = self._modelgrid._xul_to_xll(self._xul)
            else:
                xoff = 0.0
        yoff = self._modelgrid.yoffset
        if yoff is None:
            if self._yul is not None:
                yoff = self._modelgrid._yul_to_yll(self._yul)
            else:
                yoff = 0.0
        self._modelgrid.set_coord_info(
            xoff,
            yoff,
            self._modelgrid.angrot,
            self._modelgrid.crs or self._modelgrid.epsg,
        )
        self._mg_resync = not self._modelgrid.is_complete
        return self._modelgrid

    @modelgrid.setter
    def modelgrid(self, value):
        self._mg_resync = False
        self._modelgrid = value

    @property
    def solver_tols(self):
        if self.pcg is not None:
            return self.pcg.hclose, self.pcg.rclose
        elif self.nwt is not None:
            return self.nwt.headtol, self.nwt.fluxtol
        elif self.sip is not None:
            return self.sip.hclose, -999
        elif self.gmg is not None:
            return self.gmg.hclose, self.gmg.rclose
        return None

    @property
    def nlay(self):
        if self.dis:
            return self.dis.nlay
        elif self.disu:
            return self.disu.nlay
        else:
            return 0

    @property
    def nrow(self):
        if self.dis:
            return self.dis.nrow
        else:
            return 0

    @property
    def ncol(self):
        if self.dis:
            return self.dis.ncol
        else:
            return 0

    @property
    def nper(self):
        if self.dis:
            return self.dis.nper
        elif self.disu:
            return self.disu.nper
        else:
            return 0

    @property
    def ncpl(self):
        if self.dis:
            return self.dis.nrow * self.dis.ncol
        elif self.disu:
            return self.disu.ncpl
        else:
            return 0

    @property
    def nrow_ncol_nlay_nper(self):
        # structured dis
        dis = self.get_package("DIS")
        if dis:
            return dis.nrow, dis.ncol, dis.nlay, dis.nper
        # unstructured dis
        dis = self.get_package("DISU")
        if dis:
            return None, dis.nodelay.array[:], dis.nlay, dis.nper
        # no dis
        return 0, 0, 0, 0

    def get_nrow_ncol_nlay_nper(self):
        return self.nrow_ncol_nlay_nper

    def get_ifrefm(self):
        bas = self.get_package("BAS6")
        if bas:
            return bas.ifrefm
        else:
            return False

    def set_ifrefm(self, value=True):
        if not isinstance(value, bool):
            print("Error: set_ifrefm passed value must be a boolean")
            return False
        self.array_free_format = value
        bas = self.get_package("BAS6")
        if bas:
            bas.ifrefm = value
        else:
            return False

    def _set_name(self, value):
        # Overrides BaseModel's setter for name property
        super()._set_name(value)

        if self.version == "mf2k":
            for i in range(len(self.glo.extension)):
                self.glo.file_name[i] = f"{self.name}.{self.glo.extension[i]}"

        for i in range(len(self.lst.extension)):
            self.lst.file_name[i] = f"{self.name}.{self.lst.extension[i]}"

    def write_name_file(self):
        """
        Write the model name file.

        """
        fn_path = os.path.join(self.model_ws, self.namefile)
        f_nam = open(fn_path, "w")
        f_nam.write(f"{self.heading}\n")
        f_nam.write(f"#{self.modelgrid!s}")
        f_nam.write(f"; start_datetime:{self.start_datetime}\n")
        if self.version == "mf2k":
            if self.glo.unit_number[0] > 0:
                f_nam.write(
                    "{:14s} {:5d}  {}\n".format(
                        self.glo.name[0], self.glo.unit_number[0], self.glo.file_name[0]
                    )
                )
        f_nam.write(
            "{:14s} {:5d}  {}\n".format(
                self.lst.name[0], self.lst.unit_number[0], self.lst.file_name[0]
            )
        )
        f_nam.write(str(self.get_name_file_entries()))

        # write the external files
        for u, f, b, o in zip(
            self.external_units,
            self.external_fnames,
            self.external_binflag,
            self.external_output,
        ):
            if u == 0:
                continue
            replace_text = ""
            if o:
                replace_text = " REPLACE"
            if b:
                line = f"DATA(BINARY)   {u:5d}  {f}{replace_text}\n"

                f_nam.write(line)
            else:
                f_nam.write(f"DATA           {u:5d}  {f}\n")

        # write the output files
        for u, f, b in zip(self.output_units, self.output_fnames, self.output_binflag):
            if u == 0:
                continue
            if b:
                f_nam.write(f"DATA(BINARY)   {u:5d}  {f} REPLACE\n")
            else:
                f_nam.write(f"DATA           {u:5d}  {f}\n")

        # close the name file
        f_nam.close()
        return

    def set_model_units(self, iunit0=None):
        """
        Write the model name file.

        """
        if iunit0 is None:
            iunit0 = 1001

        # initialize starting unit number
        self.next_unit(iunit0)

        if self.version == "mf2k":
            # update global file unit number
            if self.glo.unit_number[0] > 0:
                self.glo.unit_number[0] = self.next_unit()

        # update lst file unit number
        self.lst.unit_number[0] = self.next_unit()

        # update package unit numbers
        for p in self.packagelist:
            p.unit_number[0] = self.next_unit()

        # update external unit numbers
        for i, iu in enumerate(self.external_units):
            if iu == 0:
                continue
            self.external_units[i] = self.next_unit()

        # update output files unit numbers
        oc = self.get_package("OC")
        output_units0 = list(self.output_units)
        for i, iu in enumerate(self.output_units):
            if iu == 0:
                continue
            iu1 = self.next_unit()
            self.output_units[i] = iu1
            # update oc files
            if oc is not None:
                if oc.iuhead == iu:
                    oc.iuhead = iu1
                elif oc.iuddn == iu:
                    oc.iuddn = iu1

        # replace value in ipakcb
        for p in self.packagelist:
            try:
                iu0 = p.ipakcb
                if iu0 in output_units0:
                    j = output_units0.index(iu0)
                    p.ipakcb = self.output_units[j]
            except:
                if self.verbose:
                    print("   could not replace value in ipakcb")

        return

    def load_results(self, **kwargs):
        # remove model if passed as a kwarg
        if "model" in kwargs:
            kwargs.pop("model")

        as_dict = False
        if "as_dict" in kwargs:
            as_dict = bool(kwargs.pop("as_dict"))

        savehead = False
        saveddn = False
        savebud = False

        # check for oc
        try:
            oc = self.get_package("OC")
            self.hext = oc.extension[1]
            self.dext = oc.extension[2]
            self.cext = oc.extension[3]
            if oc.chedfm is None:
                head_const = flopy.utils.HeadFile
            else:
                head_const = flopy.utils.FormattedHeadFile
            if oc.cddnfm is None:
                ddn_const = flopy.utils.HeadFile
            else:
                ddn_const = flopy.utils.FormattedHeadFile

            for k, lst in oc.stress_period_data.items():
                for v in lst:
                    if v.lower() == "save head":
                        savehead = True
                    if v.lower() == "save drawdown":
                        saveddn = True
                    if v.lower() == "save budget":
                        savebud = True
        except Exception as e:
            print(f"error reading output filenames from OC package: {e!s}")

        self.hpth = os.path.join(self.model_ws, f"{self.name}.{self.hext}")
        self.dpth = os.path.join(self.model_ws, f"{self.name}.{self.dext}")
        self.cpth = os.path.join(self.model_ws, f"{self.name}.{self.cext}")

        hdObj = None
        ddObj = None
        bdObj = None

        if savehead and os.path.exists(self.hpth):
            hdObj = head_const(self.hpth, model=self, **kwargs)

        if saveddn and os.path.exists(self.dpth):
            ddObj = ddn_const(self.dpth, model=self, **kwargs)
        if savebud and os.path.exists(self.cpth):
            bdObj = flopy.utils.CellBudgetFile(self.cpth, model=self, **kwargs)

        # get subsidence, if written
        subObj = None
        try:
            if self.sub is not None and "subsidence.hds" in self.sub.extension:
                idx = self.sub.extension.index("subsidence.hds")
                subObj = head_const(
                    os.path.join(self.model_ws, self.sub.file_name[idx]),
                    text="subsidence",
                )
        except Exception as e:
            print(f"error loading subsidence.hds:{e!s}")

        if as_dict:
            oudic = {}
            if subObj is not None:
                oudic["subsidence.hds"] = subObj
            if savehead and hdObj:
                oudic[self.hpth] = hdObj
            if saveddn and ddObj:
                oudic[self.dpth] = ddObj
            if savebud and bdObj:
                oudic[self.cpth] = bdObj
            return oudic
        else:
            return hdObj, ddObj, bdObj

    @classmethod
    def load(
        cls,
        f: str,
        version="mf2005",
        exe_name: Union[str, PathLike] = "mf2005",
        verbose=False,
        model_ws: Union[str, PathLike] = curdir,
        load_only=None,
        forgive=False,
        check=True,
        extra_pkgs: Optional[dict] = None,
    ):
        """
        Load an existing MODFLOW model.

        Parameters
        ----------
        f : str
            Path to MODFLOW name file to load.
        version : str, default "mf2005"
            MODFLOW version. Choose one of: "mf2k", "mf2005" (default),
            or "mfnwt". Note that this can be modified on loading
            packages unique to different MODFLOW versions.
        exe_name : str or PathLike, default "mf2005"
            MODFLOW executable name or path.
        verbose : bool, default False
            Show messages that can be useful for debugging.
        model_ws : str or PathLike, default "." (curdir)
            Model workspace path.
        load_only : list, str or None
            List of case insensitive packages to load, e.g. ["bas6", "lpf"].
            One package can also be specified, e.g. "rch". Default is None,
            which attempts to load all files. An empty list [] will not load
            any additional packages than is necessary. At a minimum, "dis" is
            always loaded.
        forgive : bool, optional
            Option to raise exceptions on package load failure, which can be
            useful for debugging. Default False.
        check : boolean, optional
            Check model input for common errors. Default True.
        extra_pkgs : dict, optional
            Add custom packages classes to mfnam_packages. Allows for loading models
            with custom packages not contained in the standard flopy distribution.


        Returns
        -------
        flopy.modflow.mf.Modflow

        Examples
        --------
        >>> import flopy
        >>> ml = flopy.modflow.Modflow.load('model.nam')

        """
        # similar to modflow command: if file does not exist , try file.nam
        namefile_path = Path(model_ws).expanduser().absolute() / f
        namefile_path_sfx = namefile_path.with_suffix(".nam")
        if not namefile_path.is_file() and namefile_path_sfx.is_file():
            namefile_path = namefile_path_sfx
        if not namefile_path.is_file():
            raise FileNotFoundError(f"cannot find name file: {namefile_path}")

        # Determine model name from 'f', without any extension or path
        modelname = namefile_path.stem
        if verbose:
            print(f"\nCreating new model with name: {modelname}\n{50 * '-'}\n")

        attribs = mfreadnam.attribs_from_namfile_header(namefile_path)
        ml = cls(
            modelname,
            version=version,
            exe_name=exe_name,
            verbose=verbose,
            model_ws=model_ws,
            extra_pkgs=extra_pkgs,
            **attribs,
        )

        files_successfully_loaded = []
        files_not_loaded = []

        # read name file
        ext_unit_dict = mfreadnam.parsenamefile(
            namefile_path, ml.mfnam_packages, verbose=verbose
        )
        if ml.verbose:
            print(
                "\n{}\nExternal unit dictionary:\n{}\n{}\n".format(
                    50 * "-", ext_unit_dict, 50 * "-"
                )
            )

        # create a dict where key is the package name, value is unitnumber
        ext_pkg_d = {v.filetype: k for (k, v) in ext_unit_dict.items()}

        # reset version based on packages in the name file
        if "NWT" in ext_pkg_d or "UPW" in ext_pkg_d:
            version = "mfnwt"
        if "GLOBAL" in ext_pkg_d:
            if version != "mf2k":
                ml.glo = ModflowGlobal(ml)
            version = "mf2k"
        if "SMS" in ext_pkg_d:
            version = "mfusg"
        if "DISU" in ext_pkg_d:
            version = "mfusg"
            ml.structured = False
        # update the modflow version
        ml.set_version(version)

        # DEPRECATED since version 3.3.4
        if ml.version == "mfusg":
            raise ValueError(
                "flopy.modflow.Modflow no longer supports mfusg; "
                "use flopy.mfusg.MfUsg() instead"
            )

        # reset unit number for glo file
        if version == "mf2k":
            if "GLOBAL" in ext_pkg_d:
                unitnumber = ext_pkg_d["GLOBAL"]
                filepth = os.path.basename(ext_unit_dict[unitnumber].filename)
                ml.glo.unit_number = [unitnumber]
                ml.glo.file_name = [filepth]
            else:
                # TODO: is this necessary? it's not done for LIST.
                ml.glo.unit_number = [0]
                ml.glo.file_name = [""]

        # reset unit number for list file
        if "LIST" in ext_pkg_d:
            unitnumber = ext_pkg_d["LIST"]
            filepth = os.path.basename(ext_unit_dict[unitnumber].filename)
            ml.lst.unit_number = [unitnumber]
            ml.lst.file_name = [filepth]

        # look for the free format flag in bas6
        bas_key = ext_pkg_d.get("BAS6")
        if bas_key is not None:
            bas = ext_unit_dict[bas_key]
            start = bas.filehandle.tell()
            line = bas.filehandle.readline()
            while line.startswith("#"):
                line = bas.filehandle.readline()
            if "FREE" in line.upper():
                ml.free_format_input = True
            bas.filehandle.seek(start)
        if verbose:
            print(f"ModflowBas6 free format:{ml.free_format_input}\n")

        # load dis
        dis_key = ext_pkg_d.get("DIS") or ext_pkg_d.get("DISU")
        if dis_key is None:
            raise KeyError("discretization entry not found in nam file")
        disnamdata = ext_unit_dict[dis_key]
        dis = disnamdata.package.load(
            disnamdata.filehandle, ml, ext_unit_dict=ext_unit_dict, check=False
        )
        files_successfully_loaded.append(disnamdata.filename)
        if ml.verbose:
            print(f"   {dis.name[0]:4s} package load...success")
        assert ml.pop_key_list.pop() == dis_key
        ext_unit_dict.pop(dis_key).filehandle.close()

        dis.start_datetime = ml._start_datetime

        if load_only is None:
            # load all packages/files
            load_only = ext_pkg_d.keys()
        else:  # check items in list
            if not isinstance(load_only, list):
                load_only = [load_only]
            not_found = []
            for i, filetype in enumerate(load_only):
                load_only[i] = filetype = filetype.upper()
                if filetype not in ext_pkg_d:
                    not_found.append(filetype)
            if not_found:
                raise KeyError(
                    "the following load_only entries were not found "
                    "in the ext_unit_dict: " + str(not_found)
                )

        # zone, mult, pval
        if "PVAL" in ext_pkg_d:
            ml.mfpar.set_pval(ml, ext_unit_dict)
            assert ml.pop_key_list.pop() == ext_pkg_d.get("PVAL")
        if "ZONE" in ext_pkg_d:
            ml.mfpar.set_zone(ml, ext_unit_dict)
            assert ml.pop_key_list.pop() == ext_pkg_d.get("ZONE")
        if "MULT" in ext_pkg_d:
            ml.mfpar.set_mult(ml, ext_unit_dict)
            assert ml.pop_key_list.pop() == ext_pkg_d.get("MULT")

        # try loading packages in ext_unit_dict
        for key, item in ext_unit_dict.items():
            if item.package is not None:
                if item.filetype in load_only:
                    package_load_args = getfullargspec(item.package.load)[0]
                    if forgive:
                        try:
                            if "check" in package_load_args:
                                item.package.load(
                                    item.filehandle,
                                    ml,
                                    ext_unit_dict=ext_unit_dict,
                                    check=False,
                                )
                            else:
                                item.package.load(
                                    item.filehandle, ml, ext_unit_dict=ext_unit_dict
                                )
                            files_successfully_loaded.append(item.filename)
                            if ml.verbose:
                                print(f"   {item.filetype:4s} package load...success")
                        except Exception as e:
                            ml.load_fail = True
                            if ml.verbose:
                                print(f"   {item.filetype:4s} package load...failed")
                                print(f"   {e!s}")
                            files_not_loaded.append(item.filename)
                    else:
                        if "check" in package_load_args:
                            item.package.load(
                                item.filehandle,
                                ml,
                                ext_unit_dict=ext_unit_dict,
                                check=False,
                            )
                        else:
                            item.package.load(
                                item.filehandle, ml, ext_unit_dict=ext_unit_dict
                            )
                        files_successfully_loaded.append(item.filename)
                        if ml.verbose:
                            print(f"   {item.filetype:4s} package load...success")
                else:
                    if ml.verbose:
                        print(f"   {item.filetype:4s} package load...skipped")
                    files_not_loaded.append(item.filename)
            elif "data" not in item.filetype.lower():
                files_not_loaded.append(item.filename)
                if ml.verbose:
                    print(f"   {item.filetype:4s} package load...skipped")
            elif "data" in item.filetype.lower():
                if ml.verbose:
                    print(f"   {item.filetype} package load...skipped")
                    print(f"      {os.path.basename(item.filename)}")
                if key not in ml.pop_key_list:
                    # do not add unit number (key) if it already exists
                    if key not in ml.external_units:
                        ml.external_fnames.append(item.filename)
                        ml.external_units.append(key)
                        ml.external_binflag.append("binary" in item.filetype.lower())
                        ml.external_output.append(False)
            else:
                raise KeyError(f"unhandled case: {key}, {item}")

        # pop binary output keys and any external file units that are now
        # internal
        for key in ml.pop_key_list:
            try:
                ml.remove_external(unit=key)
                item = ext_unit_dict.pop(key)
                if hasattr(item.filehandle, "close"):
                    item.filehandle.close()
            except KeyError:
                if ml.verbose:
                    print(
                        f"\nWARNING:\n    External file unit {key} does not "
                        "exist in ext_unit_dict."
                    )

        # write message indicating packages that were successfully loaded
        if ml.verbose:
            print("")
            print(
                f"   The following {len(files_successfully_loaded)} packages "
                "were successfully loaded."
            )
            for fname in files_successfully_loaded:
                print(f"      {os.path.basename(fname)}")
            if len(files_not_loaded) > 0:
                print(
                    f"   The following {len(files_not_loaded)} packages "
                    "were not loaded."
                )
                for fname in files_not_loaded:
                    print(f"      {os.path.basename(fname)}")
        if check:
            ml.check(f=f"{ml.name}.chk", verbose=ml.verbose, level=0)

        # return model object
        return ml
