import warnings
import logging
import pandas as pd

from functools import partial
from collections import defaultdict

from dae.utils.helpers import str2bool
from dae.variants.attributes import Role, Sex, Status
from dae.backends.raw.loader import CLILoader, CLIArgument

from dae.pedigrees.family import FamiliesData, Person, PEDIGREE_COLUMN_NAMES
from dae.pedigrees.family_role_builder import FamilyRoleBuilder
from dae.pedigrees.layout import Layout


logger = logging.getLogger(__name__)


PED_COLUMNS_REQUIRED = (
    PEDIGREE_COLUMN_NAMES["family"],
    PEDIGREE_COLUMN_NAMES["person"],
    PEDIGREE_COLUMN_NAMES["mother"],
    PEDIGREE_COLUMN_NAMES["father"],
    PEDIGREE_COLUMN_NAMES["sex"],
    PEDIGREE_COLUMN_NAMES["status"],
)


class FamiliesLoader(CLILoader):
    def __init__(self, families_filename, **params):

        super().__init__(params=params)
        self.filename = families_filename
        # TODO FIXME Params should be able to accept namedtuple instances
        # self.params["ped_sep"] = ped_sep
        self.file_format = self.params.get("ped_file_format", "pedigree")

    @staticmethod
    def load_pedigree_file(pedigree_filename, pedigree_format={}):
        pedigree_format["ped_no_role"] = str2bool(
            pedigree_format.get("ped_no_role", False)
        )
        pedigree_format["ped_no_header"] = str2bool(
            pedigree_format.get("ped_no_header", False)
        )

        ped_df = FamiliesLoader.flexible_pedigree_read(
            pedigree_filename, **pedigree_format
        )
        families = FamiliesData.from_pedigree_df(ped_df)

        FamiliesLoader._build_families_layouts(families, pedigree_format)
        FamiliesLoader._build_families_roles(families, pedigree_format)

        return families

    @staticmethod
    def _build_families_layouts(families, pedigree_format):
        ped_layout_mode = pedigree_format.get("ped_layout_mode", "load")
        if ped_layout_mode == "generate":
            for family in families.values():
                logger.debug(
                    f"building layout for family: {family.family_id}; "
                    f"{family}")
                layouts = Layout.from_family(family)
                for layout in layouts:
                    layout.apply_to_family(family)
        elif ped_layout_mode == "load":
            pass
        else:
            raise ValueError(
                f"unexpected `--ped-layout-mode` option value "
                f"`{ped_layout_mode}`"
            )

    @staticmethod
    def _build_families_roles(families, pedigree_format):
        has_unknown_roles = any(
            [
                p.role is None  # or p.role == Role.unknown
                for p in families.persons.values()
            ]
        )

        if has_unknown_roles or pedigree_format.get("ped_no_role"):
            for family in families.values():
                logger.debug(f"building family roles: {family.family_id}")
                role_build = FamilyRoleBuilder(family)
                role_build.build_roles()
            families._ped_df = None

    # @staticmethod
    # def load_simple_families_file(families_filename):
    #     ped_df = FamiliesLoader.load_simple_family_file(families_filename)
    #     return FamiliesData.from_pedigree_df(ped_df)

    def load(self):
        if self.file_format == "simple":
            return self.load_simple_families_file(self.filename)
        else:
            assert self.file_format == "pedigree"
            return self.load_pedigree_file(
                self.filename, pedigree_format=self.params
            )

    @classmethod
    def _arguments(cls):
        arguments = []
        arguments.append(CLIArgument(
            "families",
            value_type=str,
            metavar="<families filename>",
            help_text="families filename in pedigree or simple family format",
        ))
        arguments.append(CLIArgument(
            "--ped-family",
            default_value="familyId",
            help_text="specify the name of the column in the pedigree"
            " file that holds the ID of the family the person belongs to"
            " [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-person",
            default_value="personId",
            help_text="specify the name of the column in the pedigree"
            " file that holds the person's ID [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-mom",
            default_value="momId",
            help_text="specify the name of the column in the pedigree"
            " file that holds the ID of the person's mother"
            " [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-dad",
            default_value="dadId",
            help_text="specify the name of the column in the pedigree"
            " file that holds the ID of the person's father"
            " [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-sex",
            default_value="sex",
            help_text="specify the name of the column in the pedigree"
            " file that holds the sex of the person [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-status",
            default_value="status",
            help_text="specify the name of the column in the pedigree"
            " file that holds the status of the person"
            " [default: %(default)s]",

        ))
        arguments.append(CLIArgument(
            "--ped-role",
            default_value="role",
            help_text="specify the name of the column in the pedigree"
            " file that holds the role of the person"
            " [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-no-role",
            action="store_true",
            default_value=False,
            help_text="indicates that the provided pedigree file has no role "
            "column. "
            "If this argument is provided, the import tool will guess the "
            "roles "
            'of individuals and write them in a "role" column.',
        ))
        arguments.append(CLIArgument(
            "--ped-proband",
            default_value=None,
            help_text="specify the name of the column in the pedigree"
            " file that specifies persons with role `proband`;"
            " this columns is used only when"
            " option `--ped-no-role` is specified. [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-no-header",
            action="store_true",
            default_value=False,
            help_text="indicates that the provided pedigree"
            " file has no header. The pedigree column arguments"
            " will accept indices if this argument is given."
            " [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-file-format",
            default_value="pedigree",
            help_text="Families file format. It should `pedigree` or `simple`"
            "for simple family format [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-layout-mode",
            default_value="load",
            help_text="Layout mode specifies how pedigrees "
            "drawing of each family is handled."
            " Available options are `generate` and `load`. When "
            "layout mode option is set to generate the loader"
            "tryes to generate a layout for the family pedigree. "
            "When `load` is specified, the loader tries to load the layout "
            "from the layout column of the pedigree. "
            "[default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-sep",
            default_value="\t",
            raw=True,
            help_text="Families file field separator [default: `\\t`]",
        ))
        return arguments

    @classmethod
    def parse_cli_arguments(cls, argv):
        filename = argv.families
        super().parse_cli_arguments(argv, use_defaults=False)

        ped_ped_args = [
            "ped_family",
            "ped_person",
            "ped_mom",
            "ped_dad",
            "ped_sex",
            "ped_status",
            "ped_role",
            "ped_file_format",
            "ped_sep",
            "ped_proband",
            "ped_layout_mode",
        ]
        columns = set(
            [
                "ped_family",
                "ped_person",
                "ped_mom",
                "ped_dad",
                "ped_sex",
                "ped_status",
                "ped_role",
                "ped_proband",
            ]
        )
        assert argv.ped_file_format in ("simple", "pedigree")
        assert argv.ped_layout_mode in ("generate", "load")

        res = {}

        res["ped_no_header"] = str2bool(argv.ped_no_header)
        res["ped_no_role"] = str2bool(argv.ped_no_role)

        for col in ped_ped_args:
            ped_value = getattr(argv, col)
            if not res["ped_no_header"] or col not in columns:
                res[col] = ped_value
            elif ped_value is not None and col in columns:
                res[col] = int(ped_value)

        return filename, res

    @staticmethod
    def produce_header_from_indices(
        ped_family=None,
        ped_person=None,
        ped_mom=None,
        ped_dad=None,
        ped_sex=None,
        ped_status=None,
        ped_role=None,
        ped_proband=None,
        ped_layout=None,
        ped_generated=None,
        ped_not_sequenced=None,
        ped_sample_id=None,
    ):
        header = (
            (ped_family, PEDIGREE_COLUMN_NAMES["family"]),
            (ped_person, PEDIGREE_COLUMN_NAMES["person"]),
            (ped_mom, PEDIGREE_COLUMN_NAMES["mother"]),
            (ped_dad, PEDIGREE_COLUMN_NAMES["father"]),
            (ped_sex, PEDIGREE_COLUMN_NAMES["sex"]),
            (ped_status, PEDIGREE_COLUMN_NAMES["status"]),
            (ped_role, PEDIGREE_COLUMN_NAMES["role"]),
            (ped_proband, PEDIGREE_COLUMN_NAMES["proband"]),
            (ped_layout, PEDIGREE_COLUMN_NAMES["layout"]),
            (ped_generated, PEDIGREE_COLUMN_NAMES["generated"]),
            (ped_not_sequenced, PEDIGREE_COLUMN_NAMES["not_sequenced"]),
            (ped_sample_id, PEDIGREE_COLUMN_NAMES["sample id"]),
        )
        header = tuple(filter(lambda col: type(col[0]) is int, header))
        for col in header:
            assert type(col[0]) is int, col[0]
        header = tuple(sorted(header, key=lambda col: col[0]))

        return zip(*header)

    @staticmethod
    def flexible_pedigree_read(
        pedigree_filepath,
        ped_sep="\t",
        ped_no_header=False,
        ped_family="familyId",
        ped_person="personId",
        ped_mom="momId",
        ped_dad="dadId",
        ped_sex="sex",
        ped_status="status",
        ped_role="role",
        ped_proband="proband",
        ped_layout="layout",
        ped_generated="generated",
        ped_not_sequenced="not_sequenced",
        ped_sample_id="sampleId",
        ped_no_role=False,
        **kwargs,
    ):

        if type(ped_no_role) == str:
            ped_no_role = str2bool(ped_no_role)
        if type(ped_no_header) == str:
            ped_no_header = str2bool(ped_no_header)

        read_csv_func = partial(
            pd.read_csv,
            sep=ped_sep,
            index_col=False,
            skipinitialspace=True,
            converters={
                ped_role: Role.from_name,
                ped_sex: Sex.from_name,
                ped_status: Status.from_name,
                ped_generated: lambda v: str2bool(v),
                ped_not_sequenced: lambda v: str2bool(v),
                ped_proband: lambda v: str2bool(v),
            },
            dtype=str,
            comment="#",
            encoding="utf-8",
        )
        with warnings.catch_warnings(record=True) as ws:
            warnings.filterwarnings(
                "ignore",
                category=pd.errors.ParserWarning,
                message="Both a converter and dtype were specified",
            )

            if ped_no_header:
                _, file_header = FamiliesLoader.produce_header_from_indices(
                    ped_family=ped_family,
                    ped_person=ped_person,
                    ped_mom=ped_mom,
                    ped_dad=ped_dad,
                    ped_sex=ped_sex,
                    ped_status=ped_status,
                    ped_role=ped_role,
                    ped_proband=ped_proband,
                    ped_layout=ped_layout,
                    ped_generated=ped_generated,
                    ped_not_sequenced=ped_not_sequenced,
                    ped_sample_id=ped_sample_id,
                )
                ped_family = PEDIGREE_COLUMN_NAMES["family"]
                ped_person = PEDIGREE_COLUMN_NAMES["person"]
                ped_mom = PEDIGREE_COLUMN_NAMES["mother"]
                ped_dad = PEDIGREE_COLUMN_NAMES["father"]
                ped_sex = PEDIGREE_COLUMN_NAMES["sex"]
                ped_status = PEDIGREE_COLUMN_NAMES["status"]
                ped_role = PEDIGREE_COLUMN_NAMES["role"]
                ped_proband = PEDIGREE_COLUMN_NAMES["proband"]
                ped_layout = PEDIGREE_COLUMN_NAMES["layout"]
                ped_generated = PEDIGREE_COLUMN_NAMES["generated"]
                ped_not_sequenced = PEDIGREE_COLUMN_NAMES["not_sequenced"]
                ped_sample_id = PEDIGREE_COLUMN_NAMES["sample id"]
                ped_df = read_csv_func(
                    pedigree_filepath, header=None, names=file_header
                )
            else:
                ped_df = read_csv_func(pedigree_filepath)

        for w in ws:
            warnings.showwarning(w.message, w.category, w.filename, w.lineno)

        if ped_sample_id in ped_df:
            if ped_generated in ped_df or ped_not_sequenced in ped_df:

                def fill_sample_id(r):
                    if not pd.isna(r.sampleId):
                        return r.sampleId
                    else:
                        if r.generated or r.not_sequenced:
                            return None
                        else:
                            return r.personId

            else:

                def fill_sample_id(r):
                    if not pd.isna(r.sampleId):
                        return r.sampleId
                    else:
                        return r.personId

            sample_ids = ped_df.apply(
                lambda r: fill_sample_id(r), axis=1, result_type="reduce",
            )
            ped_df[ped_sample_id] = sample_ids
        else:
            sample_ids = pd.Series(data=ped_df[ped_person].values)
            ped_df[ped_sample_id] = sample_ids
        if ped_generated in ped_df:
            ped_df[ped_generated] = ped_df[ped_generated].apply(
                lambda v: v if v else None
            )
        if ped_not_sequenced in ped_df:
            ped_df[ped_not_sequenced] = ped_df[ped_not_sequenced].apply(
                lambda v: v if v else None
            )

        ped_df = ped_df.rename(
            columns={
                ped_family: PEDIGREE_COLUMN_NAMES["family"],
                ped_person: PEDIGREE_COLUMN_NAMES["person"],
                ped_mom: PEDIGREE_COLUMN_NAMES["mother"],
                ped_dad: PEDIGREE_COLUMN_NAMES["father"],
                ped_sex: PEDIGREE_COLUMN_NAMES["sex"],
                ped_status: PEDIGREE_COLUMN_NAMES["status"],
                ped_role: PEDIGREE_COLUMN_NAMES["role"],
                ped_proband: PEDIGREE_COLUMN_NAMES["proband"],
                ped_sample_id: PEDIGREE_COLUMN_NAMES["sample id"],
            }
        )

        if not set(PED_COLUMNS_REQUIRED) <= set(ped_df.columns):
            missing_columns = set(PED_COLUMNS_REQUIRED).difference(
                set(ped_df.columns)
            )
            missing_columns = ", ".join(missing_columns)
            print(f"pedigree file missing columns {missing_columns}")
            raise ValueError(
                f"pedigree file missing columns {missing_columns}"
            )
        return ped_df

    @staticmethod
    def load_simple_families_file(infile, ped_sep="\t"):
        fam_df = pd.read_csv(
            infile,
            sep=ped_sep,
            index_col=False,
            skipinitialspace=True,
            converters={
                "role": lambda r: Role.from_name(r),
                "gender": lambda s: Sex.from_name(s),
                "sex": lambda s: Sex.from_name(s),
            },
            dtype={"familyId": str, "personId": str},
            comment="#",
        )

        fam_df = fam_df.rename(
            columns={
                "gender": "sex",
                "personId": "person_id",
                "familyId": "family_id",
                "momId": "mom_id",
                "dadId": "dad_id",
                "sampleId": "sample_id",
            },
        )

        fam_df["status"] = pd.Series(index=fam_df.index, data=1)
        fam_df.loc[fam_df.role == Role.prb, "status"] = 2
        fam_df["status"] = fam_df.status.apply(lambda s: Status.from_value(s))

        fam_df["mom_id"] = pd.Series(index=fam_df.index, data="0")
        fam_df["dad_id"] = pd.Series(index=fam_df.index, data="0")

        if "sample_id" not in fam_df.columns:
            sample_ids = pd.Series(data=fam_df["person_id"].values)
            fam_df["sample_id"] = sample_ids

        families = defaultdict(list)
        for rec in fam_df.to_dict(orient="records"):
            families[rec["family_id"]].append(rec)

        result = defaultdict(list)
        for fam_id, members in families.items():
            mom_id = None
            dad_id = None
            children = []
            for member in members:
                role = member["role"]
                if role == Role.mom:
                    mom_id = member["person_id"]
                elif role == Role.dad:
                    dad_id = member["person_id"]
                else:
                    assert role in set([Role.prb, Role.sib])
                    children.append(member)
            for child in children:
                child["mom_id"] = mom_id
                child["dad_id"] = dad_id

            result[fam_id] = [Person(**member) for member in members]

        return FamiliesData.from_family_persons(result)

    @staticmethod
    def save_pedigree(families, filename):
        df = families.ped_df.copy()

        df = df.rename(
            columns={
                "person_id": "personId",
                "family_id": "familyId",
                "mom_id": "momId",
                "dad_id": "dadId",
                "sample_id": "sampleId",
            }
        )
        df.sex = df.sex.apply(lambda v: v.name)
        df.role = df.role.apply(lambda v: v.name)
        df.status = df.status.apply(lambda v: v.name)

        df.to_csv(filename, index=False, sep="\t")

    @staticmethod
    def save_families(families, filename):
        assert isinstance(families, FamiliesData)
        FamiliesLoader.save_pedigree(families, filename)

import warnings
import logging
import pandas as pd

from functools import partial
from collections import defaultdict

from dae.utils.helpers import str2bool
from dae.variants.attributes import Role, Sex, Status
from dae.backends.raw.loader import CLILoader, CLIArgument

from dae.pedigrees.family import FamiliesData, Person, PEDIGREE_COLUMN_NAMES
from dae.pedigrees.family_role_builder import FamilyRoleBuilder
from dae.pedigrees.layout import Layout


logger = logging.getLogger(__name__)


PED_COLUMNS_REQUIRED = (
    PEDIGREE_COLUMN_NAMES["family"],
    PEDIGREE_COLUMN_NAMES["person"],
    PEDIGREE_COLUMN_NAMES["mother"],
    PEDIGREE_COLUMN_NAMES["father"],
    PEDIGREE_COLUMN_NAMES["sex"],
    PEDIGREE_COLUMN_NAMES["status"],
)


class FamiliesLoader(CLILoader):
    def __init__(self, families_filename, **params):

        super().__init__(params=params)
        self.filename = families_filename
        # TODO FIXME Params should be able to accept namedtuple instances
        # self.params["ped_sep"] = ped_sep
        self.file_format = self.params.get("ped_file_format", "pedigree")

    @staticmethod
    def load_pedigree_file(pedigree_filename, pedigree_format={}):
        pedigree_format["ped_no_role"] = str2bool(
            pedigree_format.get("ped_no_role", False)
        )
        pedigree_format["ped_no_header"] = str2bool(
            pedigree_format.get("ped_no_header", False)
        )

        ped_df = FamiliesLoader.flexible_pedigree_read(
            pedigree_filename, **pedigree_format
        )
        families = FamiliesData.from_pedigree_df(ped_df)

        FamiliesLoader._build_families_layouts(families, pedigree_format)
        FamiliesLoader._build_families_roles(families, pedigree_format)

        return families

    @staticmethod
    def _build_families_layouts(families, pedigree_format):
        ped_layout_mode = pedigree_format.get("ped_layout_mode", "load")
        if ped_layout_mode == "generate":
            for family in families.values():
                logger.debug(
                    f"building layout for family: {family.family_id}; "
                    f"{family}")
                layouts = Layout.from_family(family)
                for layout in layouts:
                    layout.apply_to_family(family)
        elif ped_layout_mode == "load":
            pass
        else:
            raise ValueError(
                f"unexpected `--ped-layout-mode` option value "
                f"`{ped_layout_mode}`"
            )

    @staticmethod
    def _build_families_roles(families, pedigree_format):
        has_unknown_roles = any(
            [
                p.role is None  # or p.role == Role.unknown
                for p in families.persons.values()
            ]
        )

        if has_unknown_roles or pedigree_format.get("ped_no_role"):
            for family in families.values():
                logger.debug(f"building family roles: {family.family_id}")
                role_build = FamilyRoleBuilder(family)
                role_build.build_roles()
            families._ped_df = None

    # @staticmethod
    # def load_simple_families_file(families_filename):
    #     ped_df = FamiliesLoader.load_simple_family_file(families_filename)
    #     return FamiliesData.from_pedigree_df(ped_df)

    def load(self):
        if self.file_format == "simple":
            return self.load_simple_families_file(self.filename)
        else:
            assert self.file_format == "pedigree"
            return self.load_pedigree_file(
                self.filename, pedigree_format=self.params
            )

    @classmethod
    def _arguments(cls):
        arguments = []
        arguments.append(CLIArgument(
            "families",
            value_type=str,
            metavar="<families filename>",
            help_text="families filename in pedigree or simple family format",
        ))
        arguments.append(CLIArgument(
            "--ped-family",
            default_value="familyId",
            help_text="specify the name of the column in the pedigree"
            " file that holds the ID of the family the person belongs to"
            " [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-person",
            default_value="personId",
            help_text="specify the name of the column in the pedigree"
            " file that holds the person's ID [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-mom",
            default_value="momId",
            help_text="specify the name of the column in the pedigree"
            " file that holds the ID of the person's mother"
            " [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-dad",
            default_value="dadId",
            help_text="specify the name of the column in the pedigree"
            " file that holds the ID of the person's father"
            " [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-sex",
            default_value="sex",
            help_text="specify the name of the column in the pedigree"
            " file that holds the sex of the person [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-status",
            default_value="status",
            help_text="specify the name of the column in the pedigree"
            " file that holds the status of the person"
            " [default: %(default)s]",

        ))
        arguments.append(CLIArgument(
            "--ped-role",
            default_value="role",
            help_text="specify the name of the column in the pedigree"
            " file that holds the role of the person"
            " [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-no-role",
            action="store_true",
            default_value=False,
            help_text="indicates that the provided pedigree file has no role "
            "column. "
            "If this argument is provided, the import tool will guess the "
            "roles "
            'of individuals and write them in a "role" column.',
        ))
        arguments.append(CLIArgument(
            "--ped-proband",
            default_value=None,
            help_text="specify the name of the column in the pedigree"
            " file that specifies persons with role `proband`;"
            " this columns is used only when"
            " option `--ped-no-role` is specified. [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-no-header",
            action="store_true",
            default_value=False,
            help_text="indicates that the provided pedigree"
            " file has no header. The pedigree column arguments"
            " will accept indices if this argument is given."
            " [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-file-format",
            default_value="pedigree",
            help_text="Families file format. It should `pedigree` or `simple`"
            "for simple family format [default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-layout-mode",
            default_value="load",
            help_text="Layout mode specifies how pedigrees "
            "drawing of each family is handled."
            " Available options are `generate` and `load`. When "
            "layout mode option is set to generate the loader"
            "tryes to generate a layout for the family pedigree. "
            "When `load` is specified, the loader tries to load the layout "
            "from the layout column of the pedigree. "
            "[default: %(default)s]",
        ))
        arguments.append(CLIArgument(
            "--ped-sep",
            default_value="\t",
            raw=True,
            help_text="Families file field separator [default: `\\t`]",
        ))
        return arguments

    @classmethod
    def parse_cli_arguments(cls, argv):
        filename = argv.families
        super().parse_cli_arguments(argv, use_defaults=False)

        ped_ped_args = [
            "ped_family",
            "ped_person",
            "ped_mom",
            "ped_dad",
            "ped_sex",
            "ped_status",
            "ped_role",
            "ped_file_format",
            "ped_sep",
            "ped_proband",
            "ped_layout_mode",
        ]
        columns = set(
            [
                "ped_family",
                "ped_person",
                "ped_mom",
                "ped_dad",
                "ped_sex",
                "ped_status",
                "ped_role",
                "ped_proband",
            ]
        )
        assert argv.ped_file_format in ("simple", "pedigree")
        assert argv.ped_layout_mode in ("generate", "load")

        res = {}

        res["ped_no_header"] = str2bool(argv.ped_no_header)
        res["ped_no_role"] = str2bool(argv.ped_no_role)

        for col in ped_ped_args:
            ped_value = getattr(argv, col)
            if not res["ped_no_header"] or col not in columns:
                res[col] = ped_value
            elif ped_value is not None and col in columns:
                res[col] = int(ped_value)

        return filename, res

    @staticmethod
    def produce_header_from_indices(
        ped_family=None,
        ped_person=None,
        ped_mom=None,
        ped_dad=None,
        ped_sex=None,
        ped_status=None,
        ped_role=None,
        ped_proband=None,
        ped_layout=None,
        ped_generated=None,
        ped_not_sequenced=None,
        ped_sample_id=None,
    ):
        header = (
            (ped_family, PEDIGREE_COLUMN_NAMES["family"]),
            (ped_person, PEDIGREE_COLUMN_NAMES["person"]),
            (ped_mom, PEDIGREE_COLUMN_NAMES["mother"]),
            (ped_dad, PEDIGREE_COLUMN_NAMES["father"]),
            (ped_sex, PEDIGREE_COLUMN_NAMES["sex"]),
            (ped_status, PEDIGREE_COLUMN_NAMES["status"]),
            (ped_role, PEDIGREE_COLUMN_NAMES["role"]),
            (ped_proband, PEDIGREE_COLUMN_NAMES["proband"]),
            (ped_layout, PEDIGREE_COLUMN_NAMES["layout"]),
            (ped_generated, PEDIGREE_COLUMN_NAMES["generated"]),
            (ped_not_sequenced, PEDIGREE_COLUMN_NAMES["not_sequenced"]),
            (ped_sample_id, PEDIGREE_COLUMN_NAMES["sample id"]),
        )
        header = tuple(filter(lambda col: type(col[0]) is int, header))
        for col in header:
            assert type(col[0]) is int, col[0]
        header = tuple(sorted(header, key=lambda col: col[0]))

        return zip(*header)

    @staticmethod
    def flexible_pedigree_read(
        pedigree_filepath,
        ped_sep="\t",
        ped_no_header=False,
        ped_family="familyId",
        ped_person="personId",
        ped_mom="momId",
        ped_dad="dadId",
        ped_sex="sex",
        ped_status="status",
        ped_role="role",
        ped_proband="proband",
        ped_layout="layout",
        ped_generated="generated",
        ped_not_sequenced="not_sequenced",
        ped_sample_id="sampleId",
        ped_no_role=False,
        **kwargs,
    ):

        if type(ped_no_role) == str:
            ped_no_role = str2bool(ped_no_role)
        if type(ped_no_header) == str:
            ped_no_header = str2bool(ped_no_header)

        read_csv_func = partial(
            pd.read_csv,
            sep=ped_sep,
            index_col=False,
            skipinitialspace=True,
            converters={
                ped_role: Role.from_name,
                ped_sex: Sex.from_name,
                ped_status: Status.from_name,
                ped_generated: lambda v: str2bool(v),
                ped_not_sequenced: lambda v: str2bool(v),
                ped_proband: lambda v: str2bool(v),
            },
            dtype=str,
            comment="#",
            encoding="utf-8",
        )
        with warnings.catch_warnings(record=True) as ws:
            warnings.filterwarnings(
                "ignore",
                category=pd.errors.ParserWarning,
                message="Both a converter and dtype were specified",
            )

            if ped_no_header:
                _, file_header = FamiliesLoader.produce_header_from_indices(
                    ped_family=ped_family,
                    ped_person=ped_person,
                    ped_mom=ped_mom,
                    ped_dad=ped_dad,
                    ped_sex=ped_sex,
                    ped_status=ped_status,
                    ped_role=ped_role,
                    ped_proband=ped_proband,
                    ped_layout=ped_layout,
                    ped_generated=ped_generated,
                    ped_not_sequenced=ped_not_sequenced,
                    ped_sample_id=ped_sample_id,
                )
                ped_family = PEDIGREE_COLUMN_NAMES["family"]
                ped_person = PEDIGREE_COLUMN_NAMES["person"]
                ped_mom = PEDIGREE_COLUMN_NAMES["mother"]
                ped_dad = PEDIGREE_COLUMN_NAMES["father"]
                ped_sex = PEDIGREE_COLUMN_NAMES["sex"]
                ped_status = PEDIGREE_COLUMN_NAMES["status"]
                ped_role = PEDIGREE_COLUMN_NAMES["role"]
                ped_proband = PEDIGREE_COLUMN_NAMES["proband"]
                ped_layout = PEDIGREE_COLUMN_NAMES["layout"]
                ped_generated = PEDIGREE_COLUMN_NAMES["generated"]
                ped_not_sequenced = PEDIGREE_COLUMN_NAMES["not_sequenced"]
                ped_sample_id = PEDIGREE_COLUMN_NAMES["sample id"]
                ped_df = read_csv_func(
                    pedigree_filepath, header=None, names=file_header
                )
            else:
                ped_df = read_csv_func(pedigree_filepath)

        for w in ws:
            warnings.showwarning(w.message, w.category, w.filename, w.lineno)

        if ped_sample_id in ped_df:
            if ped_generated in ped_df or ped_not_sequenced in ped_df:

                def fill_sample_id(r):
                    if not pd.isna(r.sampleId):
                        return r.sampleId
                    else:
                        if r.generated or r.not_sequenced:
                            return None
                        else:
                            return r.personId

            else:

                def fill_sample_id(r):
                    if not pd.isna(r.sampleId):
                        return r.sampleId
                    else:
                        return r.personId

            sample_ids = ped_df.apply(
                lambda r: fill_sample_id(r), axis=1, result_type="reduce",
            )
            ped_df[ped_sample_id] = sample_ids
        else:
            sample_ids = pd.Series(data=ped_df[ped_person].values)
            ped_df[ped_sample_id] = sample_ids
        if ped_generated in ped_df:
            ped_df[ped_generated] = ped_df[ped_generated].apply(
                lambda v: v if v else None
            )
        if ped_not_sequenced in ped_df:
            ped_df[ped_not_sequenced] = ped_df[ped_not_sequenced].apply(
                lambda v: v if v else None
            )

        ped_df = ped_df.rename(
            columns={
                ped_family: PEDIGREE_COLUMN_NAMES["family"],
                ped_person: PEDIGREE_COLUMN_NAMES["person"],
                ped_mom: PEDIGREE_COLUMN_NAMES["mother"],
                ped_dad: PEDIGREE_COLUMN_NAMES["father"],
                ped_sex: PEDIGREE_COLUMN_NAMES["sex"],
                ped_status: PEDIGREE_COLUMN_NAMES["status"],
                ped_role: PEDIGREE_COLUMN_NAMES["role"],
                ped_proband: PEDIGREE_COLUMN_NAMES["proband"],
                ped_sample_id: PEDIGREE_COLUMN_NAMES["sample id"],
            }
        )

        if not set(PED_COLUMNS_REQUIRED) <= set(ped_df.columns):
            missing_columns = set(PED_COLUMNS_REQUIRED).difference(
                set(ped_df.columns)
            )
            missing_columns = ", ".join(missing_columns)
            print(f"pedigree file missing columns {missing_columns}")
            raise ValueError(
                f"pedigree file missing columns {missing_columns}"
            )
        return ped_df

    @staticmethod
    def load_simple_families_file(infile, ped_sep="\t"):
        fam_df = pd.read_csv(
            infile,
            sep=ped_sep,
            index_col=False,
            skipinitialspace=True,
            converters={
                "role": lambda r: Role.from_name(r),
                "gender": lambda s: Sex.from_name(s),
                "sex": lambda s: Sex.from_name(s),
            },
            dtype={"familyId": str, "personId": str},
            comment="#",
        )

        fam_df = fam_df.rename(
            columns={
                "gender": "sex",
                "personId": "person_id",
                "familyId": "family_id",
                "momId": "mom_id",
                "dadId": "dad_id",
                "sampleId": "sample_id",
            },
        )

        fam_df["status"] = pd.Series(index=fam_df.index, data=1)
        fam_df.loc[fam_df.role == Role.prb, "status"] = 2
        fam_df["status"] = fam_df.status.apply(lambda s: Status.from_value(s))

        fam_df["mom_id"] = pd.Series(index=fam_df.index, data="0")
        fam_df["dad_id"] = pd.Series(index=fam_df.index, data="0")

        if "sample_id" not in fam_df.columns:
            sample_ids = pd.Series(data=fam_df["person_id"].values)
            fam_df["sample_id"] = sample_ids

        families = defaultdict(list)
        for rec in fam_df.to_dict(orient="records"):
            families[rec["family_id"]].append(rec)

        result = defaultdict(list)
        for fam_id, members in families.items():
            mom_id = None
            dad_id = None
            children = []
            for member in members:
                role = member["role"]
                if role == Role.mom:
                    mom_id = member["person_id"]
                elif role == Role.dad:
                    dad_id = member["person_id"]
                else:
                    assert role in set([Role.prb, Role.sib])
                    children.append(member)
            for child in children:
                child["mom_id"] = mom_id
                child["dad_id"] = dad_id

            result[fam_id] = [Person(**member) for member in members]

        return FamiliesData.from_family_persons(result)

    @staticmethod
    def save_pedigree(families, filename):
        df = families.ped_df.copy()

        df = df.rename(
            columns={
                "person_id": "personId",
                "family_id": "familyId",
                "mom_id": "momId",
                "dad_id": "dadId",
                "sample_id": "sampleId",
            }
        )
        df.sex = df.sex.apply(lambda v: v.name)
        df.role = df.role.apply(lambda v: v.name)
        df.status = df.status.apply(lambda v: v.name)

        df.to_csv(filename, index=False, sep="\t")

    @staticmethod
    def save_families(families, filename):
        assert isinstance(families, FamiliesData)
        FamiliesLoader.save_pedigree(families, filename)

# TODO FIXME Params should be able to accept namedtuple instances # self.params["ped_sep"] = ped_sep # or p.role == Role.unknown # @staticmethod # def load_simple_families_file(families_filename): #     ped_df = FamiliesLoader.load_simple_family_file(families_filename) #     return FamiliesData.from_pedigree_df(ped_df)