import logging
import os
from collections.abc import Iterable
from itertools import chain, groupby
from operator import attrgetter

import numpy as np

from ada.core.utils import NewLine, bool2text
from ada.fem import FemSet, HistOutput, Load, Step, Surface
from ada.fem.io.utils import _folder_prep
from ada.sections import SectionCat

__all__ = ["main_inp_str", "to_fem"]

main_inp_str = r"""*Heading
** Generated by:
** Assembly For Design and Analysis (ADA) FEM IO (Abaqus)
*Preprint, echo=NO, model=NO, history=NO, contact=NO
{constr_ctrl}
**
** -------------------------------------------------------
** PARTS
** -------------------------------------------------------
{part_str}
** -------------------------------------------------------
** ASSEMBLY
** -------------------------------------------------------
*Assembly, name=Assembly
**
{instance_str}
**
*INCLUDE,INPUT=core_input_files\connectors.inp
*INCLUDE,INPUT=core_input_files\constraints.inp
*INCLUDE,INPUT=core_input_files\assembly_data.inp
**
*End Assembly
** -------------------------------------------------------
** Connector Sections and Amplitudes
** -------------------------------------------------------
{ampl_str}
{consec_str}
**
** -------------------------------------------------------
** INTERACTION PROPERTIES
** -------------------------------------------------------
{int_prop_str}
**
** -------------------------------------------------------
** MATERIALS
** -------------------------------------------------------
{mat_str}
**
** -------------------------------------------------------
** BOUNDARY CONDITIONS
** -------------------------------------------------------
{fix_str}
**
** -------------------------------------------------------
** INTERACTIONS
** -------------------------------------------------------
{interact_str}
**
** -------------------------------------------------------
** STEPS
** -------------------------------------------------------
{step_str}"""

log_fin = "Please check your result and input. This is not a validated method of solving this issue"

_aba_bc_map = dict(
    displacement="Displacement/Rotation",
    velocity="Velocity/Angular velocity",
    connector_displacement="Connector displacement",
    connector_velocity="Connector velocity",
)
_valid_aba_bcs = list(_aba_bc_map.values()) + [
    "symmetry/antisymmetry/encastre",
    "displacement/rotation",
    "velocity/angular velocity",
]


def to_fem(
    assembly,
    name,
    scratch_dir=None,
    metadata=None,
    execute=False,
    run_ext=False,
    cpus=2,
    gpus=None,
    overwrite=False,
    exit_on_complete=True,
):
    """

    :param assembly:
    :param name:
    :param scratch_dir:
    :param metadata:
    :param execute:
    :param run_ext:
    :param cpus:
    :param gpus:
    :param overwrite:
    :param exit_on_complete:

    """
    from .execute import run_abaqus

    if metadata is None:
        metadata = dict()

    if "ecc_to_mpc" not in metadata.keys():
        metadata["ecc_to_mpc"] = True

    if "hinges_to_coupling" not in metadata.keys():
        metadata["hinges_to_coupling"] = True

    a = AbaqusWriter(assembly)
    a.write(name, scratch_dir, metadata, overwrite=overwrite)
    print(f'Created an Abaqus input deck at "{a.analysis_path}"')
    # Create run batch files and if execute=True run the analysis
    inp_path = (a.analysis_path / name).with_suffix(".inp")

    run_abaqus(
        inp_path,
        cpus=cpus,
        gpus=gpus,
        run_ext=run_ext,
        manifest=metadata,
        subr_path=a._subr_path,
        execute=execute,
        exit_on_complete=exit_on_complete,
    )
    return a.analysis_path


class AbaqusWriter:
    """

    :type assembly: ada.Assembly
    """

    _subr_path = None
    _subroutine = None
    _imperfections = str()
    _node_hist_out = ["UT", "VT", "AT"]
    _con_hist_out = ["CTF", "CVF", "CP", "CU"]
    _rf_node_out = ["RT"]
    analysis_path = None
    parts_and_assemblies = True

    def __init__(self, assembly):
        self.assembly = assembly

    def write(self, name, scratch_dir=None, metadata=None, overwrite=False):
        """
        Build the Abaqus Analysis folder (and optionally start the analysis)

        :param name: Give your analysis a name.
        :param metadata: Attach additional info to this specific analysis
        :param overwrite: Allow folder overwrite. Will delete folder before trying to build stack.
        :param scratch_dir: Set the scratch dir
        :type metadata: str
        :type name: str
        :type overwrite: bool
        """
        print("creating: {0}".format(name))

        self.analysis_path = _folder_prep(scratch_dir, name, overwrite)

        self.assembly.metadata["info"] = metadata

        # Sensors
        list(map(self.write_sensor, self.assembly.fem.sensors.values()))

        for part in self.assembly.get_all_subparts():
            if len(part.fem.elements) + len(part.fem.connectors) == 0:
                continue

            part.fem.convert_hinges_2_couplings()
            part.fem.convert_ecc_to_mpc()

            self.write_part_bulk(part)

        core_dir = self.analysis_path / r"core_input_files"
        os.makedirs(core_dir)

        # Main Input File
        with open(self.analysis_path / f"{name}.inp", "w") as d:
            d.write(self.main_inp_str)

        # Connector Sections
        with open(core_dir / "connector_sections.inp", "w") as d:
            d.write(self.connector_sections_str)

        # Connectors
        with open(core_dir / "connectors.inp", "w") as d:
            d.write(self.connectors_str if len(self.assembly.fem.connectors) > 0 else "**")

        # Constraints
        with open(core_dir / "constraints.inp", "w") as d:
            d.write(self.constraints_str if len(self.assembly.fem.constraints) > 0 else "**")

        # Assembly data
        with open(core_dir / "assembly_data.inp", "w") as d:
            if len(self.assembly.fem.nodes) > 0:
                assembly_nodes_str = (
                    "*Node\n"
                    + "".join(
                        [
                            f"{no.id:>7}, {no.x:>13.6f}, {no.y:>13.6f}, {no.z:>13.6f}\n"
                            for no in sorted(self.assembly.fem.nodes, key=attrgetter("id"))
                        ]
                    ).rstrip()
                )
            else:
                assembly_nodes_str = "** No Nodes"
            d.write(f"{assembly_nodes_str}\n{self.nsets_str}\n{self.elsets_str}\n{self.surfaces_str}\n")
            d.write(orientations_str(self.assembly, self))

        # Amplitude data
        with open(core_dir / "amplitude_data.inp", "w") as d:
            d.write(self.amplitude_str)

        # Interaction Properties
        with open(core_dir / "interaction_prop.inp", "w") as d:
            d.write(self.int_prop_str)

        # Interactions data
        self.eval_interactions()
        with open(core_dir / "interactions.inp", "a") as d:
            d.write(self.predefined_fields_str)

        # Materials data
        with open(core_dir / "materials.inp", "w") as d:
            d.write(self.materials_str)

        # Boundary Condition data
        with open(core_dir / "bc_data.inp", "w") as d:
            d.write(self.bc_str)

        # Analysis steps
        for step_in in self.assembly.fem.steps:
            self.write_step(step_in)

    def eval_interactions(self):
        if len(self.assembly.fem.steps) > 0:
            initial_step = self.assembly.fem.steps[0]
            if initial_step.type == "explicit":
                for interact in self.assembly.fem.interactions.values():
                    if interact.name not in initial_step.interactions.keys():
                        initial_step.add_interaction(interact)
                        return
        with open(self.analysis_path / "core_input_files/interactions.inp", "w") as d:
            if self.interact_str != "":
                d.write(self.interact_str)
                d.write("\n")

    def write_step(self, step_in):
        """

        :param step_in:
        :type step_in: Step
        """
        step_str = AbaStep(step_in).str
        with open(self.analysis_path / "core_input_files" / f"step_{step_in.name}.inp", "w") as d:
            d.write(step_str)
            if "*End Step" not in step_str:
                d.write("*End Step\n")

    def write_part_bulk(self, part_in):
        """

        :param part_in:
        :type part_in: Part
        """
        bulk_path = self.analysis_path / f"bulk_{part_in.name}"
        bulk_file = bulk_path / "aba_bulk.inp"
        os.makedirs(bulk_path, exist_ok=True)

        if part_in.fem.initial_state is not None:
            with open(bulk_file, "w") as d:
                d.write("** This part is replaced by an initial state step")
        else:
            fempart = AbaqusPartWriter(part_in)
            with open(bulk_file, "w") as d:
                d.write(fempart.bulk_str)

    def write_sensor(self, sensor_in):
        """

        :param sensor_in:
        :type sensor_in: FemSet
        """
        for step in self.assembly.fem.steps:
            step.add_history_output(
                HistOutput(
                    "{}_hist".format(sensor_in.name),
                    sensor_in,
                    "node",
                    self._node_hist_out,
                )
            )

    @staticmethod
    def part_inp_str(part):
        """

        :param part:
        :type part: Part
        :return: str
        """
        return """**\n*Part, name={name}\n*INCLUDE,INPUT=bulk_{name}\\{inp_file}\n*End Part\n**""".format(
            name=part.name, inp_file="aba_bulk.inp"
        )

    def inst_inp_str(self, part):
        """

        :param part:
        :type part: ada.Part
        :return: str
        """
        if part.fem.initial_state is not None:
            import shutil

            istep = part.fem.initial_state
            analysis_name = os.path.basename(istep.initial_state_file.replace(".inp", ""))
            source_dir = os.path.dirname(istep.initial_state_file)
            for f in os.listdir(source_dir):
                if analysis_name in f:
                    dest_file = os.path.join(self.analysis_path, os.path.basename(f))
                    shutil.copy(os.path.join(source_dir, f), dest_file)
            return f"""*Instance, library={analysis_name}, instance={istep.initial_state_part.fem.instance_name}
**
** PREDEFINED FIELD
**
** Name: {part.fem.initial_state.name}   Type: Initial State
*Import, state=yes, update=no
*End Instance"""
        else:
            return f"""**\n*Instance, name={part.fem.instance_name}, part={part.name}\n*End Instance"""

    @staticmethod
    def step_inp_str(step):
        """

        :param step:
        :type step: Step
        :return: str
        """
        return f"""*INCLUDE,INPUT=core_input_files\\step_{step.name}.inp"""

    @property
    def constraint_control(self):
        constraint_ctrl_on = True
        for step in self.assembly.fem.steps:
            if step.type == "explicit":
                constraint_ctrl_on = False
        return "**" if constraint_ctrl_on is False else "*constraint controls, print=yes"

    @property
    def main_inp_str(self):
        """
        Main input file for Abaqus analysis
        """

        def skip_if_this(p):
            if p.fem.initial_state is not None:
                return False
            return len(p.fem.elements) + len(p.fem.connectors) != 0

        def inst_skip(p):
            if p.fem.initial_state is not None:
                return True
            return len(p.fem.elements) + len(p.fem.connectors) != 0

        part_str = "\n".join(map(self.part_inp_str, filter(skip_if_this, self.assembly.get_all_subparts())))
        instance_str = "\n".join(map(self.inst_inp_str, filter(inst_skip, self.assembly.get_all_subparts())))
        step_str = (
            "\n".join(list(map(self.step_inp_str, self.assembly.fem.steps))).rstrip()
            if len(self.assembly.fem.steps) > 0
            else "** No Steps added"
        )
        incl = "*INCLUDE,INPUT=core_input_files"
        ampl_str = f"\n{incl}\\amplitude_data.inp" if self.amplitude_str != "" else "**"
        consec_str = f"\n{incl}\\connector_sections.inp" if self.connector_sections_str != "" else "**"
        int_prop_str = f"{incl}\\interaction_prop.inp" if self.int_prop_str != "" else "**"
        if self.interact_str != "" or self.predefined_fields_str != "":
            interact_str = f"{incl}\\interactions.inp"
        else:
            interact_str = "**"
        mat_str = f"{incl}\\materials.inp" if self.materials_str != "" else "**"
        fix_str = f"{incl}\\bc_data.inp" if self.bc_str != "" else "**"

        return main_inp_str.format(
            part_str=part_str,
            instance_str=instance_str.rstrip(),
            mat_str=mat_str,
            fix_str=fix_str,
            step_str=step_str,
            ampl_str=ampl_str,
            consec_str=consec_str,
            int_prop_str=int_prop_str,
            interact_str=interact_str,
            constr_ctrl=self.constraint_control,
        )

    @property
    def elsets_str(self):
        return (
            "\n".join([aba_set_str(el, self) for el in self.assembly.fem.elsets.values()]).rstrip()
            if len(self.assembly.fem.elsets) > 0
            else "** No element sets"
        )

    @property
    def nsets_str(self):
        return (
            "\n".join([aba_set_str(no, self) for no in self.assembly.fem.nsets.values()]).rstrip()
            if len(self.assembly.fem.nsets) > 0
            else "** No node sets"
        )

    @property
    def materials_str(self):
        return "\n".join([material_str(mat) for mat in self.assembly.materials])

    @property
    def surfaces_str(self):
        return (
            "\n".join([surface_str(s, self) for s in self.assembly.fem.surfaces.values()])
            if len(self.assembly.fem.surfaces) > 0
            else "** No Surfaces"
        )

    @property
    def constraints_str(self):
        return (
            "\n".join([AbaConstraint(c, self).str for c in self.assembly.fem.constraints])
            if len(self.assembly.fem.constraints) > 0
            else "** No Constraints"
        )

    @property
    def connector_sections_str(self):
        return "\n".join([connector_section_str(consec) for consec in self.assembly.fem.connector_sections.values()])

    @property
    def connectors_str(self):
        return "\n".join([connector_str(con, self) for con in self.assembly.fem.connectors.values()])

    @property
    def amplitude_str(self):
        return "\n".join([amplitude_str(ampl) for ampl in self.assembly.fem.amplitudes.values()])

    @property
    def interact_str(self):
        return "\n".join([interaction_str(interact, self) for interact in self.assembly.fem.interactions.values()])

    @property
    def int_prop_str(self):
        iprop_str = "\n".join([interaction_prop_str(iprop) for iprop in self.assembly.fem.intprops.values()])
        smoothings = self.assembly.fem.metadata.get("surf_smoothing", None)
        if smoothings is not None:
            iprop_str += "\n"
            for smooth in smoothings:
                name = smooth["name"]
                iprop_str += f"*Surface Smoothing, name={name}\n"
                iprop_str += smooth["bulk"] + "\n"
        return iprop_str

    @property
    def predefined_fields_str(self):
        def eval_fields(pre_field):
            """

            :param pre_field:
            :type pre_field: ada.fem.PredefinedField
            """
            return True if pre_field.type != "INITIAL STATE" else False

        return "\n".join(
            [
                predefined_field_str(prefield)
                for prefield in filter(eval_fields, self.assembly.fem.predefined_fields.values())
            ]
        )

    @property
    def bc_str(self):
        return "\n".join(
            chain.from_iterable(
                (
                    [bc_str(bc, self) for bc in self.assembly.fem.bcs],
                    [bc_str(bc, self) for p in self.assembly.get_all_parts_in_assembly() for bc in p.fem.bcs],
                )
            )
        )

    def __repr__(self):
        return "AbaqusWriter()"


class AbaqusPartWriter:
    def __init__(self, part):
        """

        :param part:
        :type part: ada.Part
        """
        self.part = part

    @property
    def bulk_str(self):
        return f"""** Abaqus Part {self.part.name}
** Exported using ADA OpenSim
*NODE
{self.nodes_str}
{self.elements_str}
{self.elsets_str}
{self.nsets_str}
{self.solid_sec_str}
{self.shell_sec_str}
{self.beam_sec_str}
{self.mass_str}
{self.surfaces_str}
{self.constraints_str}
{self.springs_str}""".rstrip()

    @property
    def solid_sec_str(self):
        solid_secs = [AbaSection(sec, self).str for sec in self.part.fem.sections.solids]
        return "\n".join(solid_secs).rstrip() if len(solid_secs) > 0 else "** No solid sections"

    @property
    def shell_sec_str(self):
        shell_secs = [AbaSection(sec, self).str for sec in self.part.fem.sections.shells]
        return "\n".join(shell_secs).rstrip() if len(shell_secs) > 0 else "** No shell sections"

    @property
    def beam_sec_str(self):
        beam_secs = [AbaSection(sec, self).str for sec in self.part.fem.sections.beams]
        return "\n".join(beam_secs).rstrip() if len(beam_secs) > 0 else "** No beam sections"

    @property
    def elsets_str(self):
        if len(self.part.fem.elsets) > 0:
            return "\n".join([aba_set_str(el, self) for el in self.part.fem.elsets.values()]).rstrip()
        else:
            return "** No element sets"

    @property
    def nsets_str(self):
        if len(self.part.fem.nsets) > 0:
            return "\n".join([aba_set_str(no, self) for no in self.part.fem.nsets.values()]).rstrip()
        else:
            return "** No node sets"

    @property
    def nodes_str(self):
        f = "{nid:>7}, {x:>13.6f}, {y:>13.6f}, {z:>13.6f}"
        return (
            "\n".join(
                [
                    f.format(nid=no.id, x=no[0], y=no[1], z=no[2])
                    for no in sorted(self.part.fem.nodes, key=attrgetter("id"))
                ]
            ).rstrip()
            if len(self.part.fem.nodes) > 0
            else "** No Nodes"
        )

    @property
    def elements_str(self):
        part_el = self.part.fem.elements
        grouping = groupby(part_el, key=attrgetter("type", "elset"))
        return (
            "".join([els for els in [elwriter(x, elements) for x, elements in grouping] if els is not None]).rstrip()
            if len(self.part.fem.elements) > 0
            else "** No elements"
        )

    @property
    def mass_str(self):
        return (
            "\n".join([mass_str(m) for m in self.part.fem.masses.values()])
            if len(self.part.fem.masses) > 0
            else "** No Masses"
        )

    @property
    def surfaces_str(self):
        if len(self.part.fem.surfaces) > 0:
            return "\n".join([surface_str(s, self) for s in self.part.fem.surfaces.values()])
        else:
            return "** No Surfaces"

    @property
    def constraints_str(self):
        return (
            "\n".join([AbaConstraint(c, self).str for c in self.part.fem.constraints])
            if len(self.part.fem.constraints) > 0
            else "** No Constraints"
        )

    @property
    def springs_str(self):
        return (
            "\n".join([spring_str(c) for c in self.part.fem.springs.values()])
            if len(self.part.fem.springs) > 0
            else "** No Springs"
        )

    @property
    def instance_move_str(self):
        if self.part.fem.metadata["move"] is not None:
            move = self.part.fem.metadata["move"]
            mo_str = "\n " + ", ".join([str(x) for x in move])
        else:
            mo_str = "\n 0.,        0.,           0."

        if self.part.fem.metadata["rotate"] is not None:
            rotate = self.part.fem.metadata["rotate"]
            vecs = ", ".join([str(x) for x in rotate[0]])
            vece = ", ".join([str(x) for x in rotate[1]])
            angle = rotate[2]
            move_str = """{move_str}\n {vecs}, {vece}, {angle}""".format(
                move_str=mo_str, vecs=vecs, vece=vece, angle=angle
            )
        else:
            move_str = "" if mo_str == "0.,        0.,           0." else mo_str
        return move_str


class AbaStep:
    """
    :type step: ada.fem.Step
    """

    def __init__(self, step):
        self.step = step

    @property
    def dynamic_implicit_str(self):
        """
        Add a dynamic implicit step
        """

        return f"""*Step, name={self.step.name}, nlgeom={bool2text(self.step.nl_geom)}, inc={self.step.total_incr}
*Dynamic,application={self.step.dyn_type}, INITIAL={bool2text(self.step.init_accel_calc)}
 {self.step.init_incr},{self.step.total_time},{self.step.min_incr}, {self.step.max_incr}"""

    @property
    def explicit_str(self):

        return f"""*Step, name={self.step.name}, nlgeom={bool2text(self.step.nl_geom)}
*Dynamic, Explicit
, {self.step.total_time}
*Bulk Viscosity
    0.06, 1.2"""

    @property
    def static_str(self):
        """
        A static implicit step
        """

        static_str = ""
        stabilize = self.step.stabilize
        if stabilize is None:
            pass
        elif type(stabilize) is dict:
            stabkeys = list(stabilize.keys())
            if "energy" in stabkeys:
                static_str += ", stabilize={}, allsdtol={}".format(stabilize["energy"], stabilize["allsdtol"])
            elif "damping" in stabkeys:
                static_str += ", stabilize, factor={}, allsdtol={}".format(stabilize["damping"], stabilize["allsdtol"])
            elif "continue" in stabkeys:
                if stabilize["continue"] == "YES":
                    static_str += ", stabilize, continue={}".format(stabilize["continue"])
            else:
                static_str += ", stabilize=0.0002, allsdtol=0.05"
                print(
                    'Unrecognized stabilization type "{}". Will revert to energy stabilization "{}"'.format(
                        stabkeys[0], static_str
                    )
                )
        elif stabilize is True:
            static_str += ", stabilize=0.0002, allsdtol=0.05"
        elif stabilize is False:
            pass
        else:
            static_str += ", stabilize=0.0002, allsdtol=0.05"
            logging.error(
                "Unrecognized stabilize input. Can be bool, dict or None. "
                'Reverting to default stabilizing type "energy"'
            )
        line1 = (
            f"*Step, name={self.step.name}, nlgeom={bool2text(self.step.nl_geom)}, "
            f"unsymm={bool2text(self.step.unsymm)}, inc={self.step.total_incr}"
        )

        return f"""{line1}
*Static{static_str}
 {self.step.init_incr}, {self.step.total_time}, {self.step.min_incr}, {self.step.max_incr}"""

    @property
    def eigenfrequency_str(self):
        """
        An eigenfrequency step
        """
        return f"""** ----------------------------------------------------------------
**
** STEP: eig
**
*Step, name=eig, nlgeom=NO, perturbation
*Frequency, eigensolver=Lanczos, sim=NO, acoustic coupling=on, normalization=displacement
{self.step.eigenmodes}, , , , ,
"""

    @property
    def complex_eig_str(self):
        """
        A complex eigenfrequency analysis
        """
        return f"""** ----------------------------------------------------------------
**
** STEP: complex_eig
**
*Step, name=complex_eig, nlgeom=NO, perturbation, unsymm=YES
*Complex Frequency, friction damping=NO
{self.step.eigenmodes}, , ,
"""

    @property
    def steady_state_response_str(self):
        """
        A Steady state response analysis
        """

        if self.step.nodeid is None:
            raise ValueError("Please define a nodeid for the steady state load")

        return f"""** ----------------------------------------------------------------
*STEP,NAME=Response_Analysis_{self.step.fmin}_{self.step.fmax}Hz
*STEADY STATE DYNAMICS, DIRECT, INTERVAL=RANGE
{add_freq_range(self.step.fmin, self.step.fmax)}
*GLOBAL DAMPING, ALPHA={self.step.alpha} , BETA={self.step.beta}
**
*LOAD CASE, NAME=LC1
*CLOAD, OP=NEW
{self.step.nodeid},2, 1
*END LOAD CASE
**
*OUTPUT, FIELD, FREQUENCY=1
*NODE OUTPUT
U
**
*OUTPUT, HISTORY, FREQUENCY=1
*NODE OUTPUT, NSET=accel_data_set
UT, AT, TU, TA
**
"""

    @property
    def _hist_output_str(self):
        return (
            "\n".join([hist_output_str(hs) for hs in self.step.hist_outputs])
            if len(self.step.hist_outputs) > 0
            else "**"
        )

    @property
    def _field_output_str(self):
        return (
            "\n".join([field_output_str(fs) for fs in self.step.field_outputs])
            if len(self.step.field_outputs) > 0
            else "**"
        )

    @property
    def interactions_str(self):
        return "\n".join([interaction_str(interact, self) for interact in self.step.interactions.values()])

    @property
    def step_data_str(self):
        if self.step.type.lower() == "static":
            return self.static_str
        elif self.step.type.lower() in ["quasi_static", "dynamic"]:
            return self.dynamic_implicit_str
        elif self.step.type.lower() == "explicit":
            return self.explicit_str

        elif self.step.type.lower() == "eigenfrequency":
            return self.eigenfrequency_str

        elif self.step.type.lower() == "response_analysis":
            return self.steady_state_response_str

        elif self.step.type.lower() == "complex_eig":
            return self.complex_eig_str

        else:
            raise ValueError(f"Unrecognized step type {self.step.type}.")

    @property
    def bc_str(self):
        bcstr = ""
        for bcid, bc_ in self.step.bcs.items():
            bcstr += "\n" if "\n" not in bcstr[-2:] != "" else ""
            bcstr += bc_str(bc_, self)

        return bcstr

    @property
    def load_str(self):
        return "\n".join([load_str(l) for l in self.step.loads])

    @property
    def restart_request_str(self):
        return (
            f"*Restart, write, frequency={self.step.restart_int}"
            if self.step.restart_int is not None
            else "** No Restart Requests"
        )

    @property
    def str(self):
        if "aba_inp" in self.step.metadata.keys():
            return self.step.metadata["aba_inp"]
        load_str = "** No Loads" if self.load_str == "" else self.load_str.rstrip()
        bc_str = "** No BCs" if self.bc_str == "" else self.bc_str.rstrip()
        int_str = "** No Interactions" if len(self.step.interactions) == 0 else self.interactions_str
        app_str = self.step.metadata["append"] if "append" in self.step.metadata.keys() else "**"

        return f"""**
** STEP: {self.step.name}
**
{self.step_data_str}
**
** BOUNDARY CONDITIONS
**
{bc_str}
**
** LOADS
**
{load_str}
**
** INTERACTIONS
**
{int_str}
**
** OUTPUT REQUESTS
**
{self.restart_request_str}
{self._hist_output_str}
{self._field_output_str}
{app_str}
"""


class AbaSection:
    """

    :type fem_sec: ada.fem.FemSection
    """

    def __init__(self, fem_sec, fem_writer):
        self.fem_sec = fem_sec
        self._fem_writer = fem_writer

    @property
    def _temp_str(self):
        _temperature = self.fem_sec.metadata["temperature"] if "temperature" in self.fem_sec.metadata.keys() else None
        return _temperature if _temperature is not None else "GRADIENT"

    @property
    def section_data(self):
        if "section_type" in self.fem_sec.metadata.keys():
            return self.fem_sec.metadata["section_type"]

        if self.fem_sec.section.type in SectionCat.circular:
            return "CIRC"
        elif self.fem_sec.section.type in SectionCat.igirders or self.fem_sec.section.type in SectionCat.iprofiles:
            return "I"
        elif self.fem_sec.section.type in SectionCat.box:
            return "BOX"
        elif self.fem_sec.section.type in SectionCat.general:
            return "GENERAL"
        elif self.fem_sec.section.type in SectionCat.tubular:
            return "PIPE"
        elif self.fem_sec.section.type in SectionCat.angular:
            return "L"
        else:
            raise Exception(f'Section type "{self.fem_sec.section.type}" is not added to Abaqus beam export yet')

    @property
    def props(self):
        """
        To understand the local coordinate system and n1 direction

        https://abaqus-docs.mit.edu/2017/English/SIMACAEELMRefMap/simaelm-c-beamcrosssection.htm
        """

        n1 = ", ".join(str(x) for x in self.fem_sec.local_y)
        if "line1" in self.fem_sec.metadata.keys():
            return self.fem_sec.metadata["line1"] + f"\n{n1}"

        sec = self.fem_sec.section
        if self.section_data == "CIRC":
            return f"{sec.r}\n {n1}"
        elif self.section_data == "I":
            if sec.t_fbtn + sec.t_w > min(sec.w_top, sec.w_btn):
                new_width = sec.t_fbtn + sec.t_w + 5e-3
                if sec.w_btn == min(sec.w_top, sec.w_btn):
                    sec.w_btn = new_width
                else:
                    sec.w_top = new_width
                logging.error(f"For {self.fem_sec.name}: t_fbtn + t_w > min(w_top, w_btn). {log_fin}")
            return f"{sec.h / 2}, {sec.h}, {sec.w_btn}, {sec.w_top}, {sec.t_fbtn}, {sec.t_ftop}, {sec.t_w}\n {n1}"
        elif self.section_data == "BOX":
            if sec.t_w * 2 > min(sec.w_top, sec.w_btn):
                raise ValueError("Web thickness cannot be larger than section width")
            return f"{sec.w_top}, {sec.h}, {sec.t_w}, {sec.t_ftop}, {sec.t_w}, {sec.t_fbtn}\n {n1}"
        elif self.section_data == "GENERAL":
            gp = sec.properties
            mat = self.fem_sec.material.model
            if gp.Ix <= 0.0:
                gp.Ix = 1
                logging.error(f"Section {self.fem_sec.name} Ix <= 0.0. Changing to 2. {log_fin}")
            if gp.Iy <= 0.0:
                gp.Iy = 2
                logging.error(f"Section {self.fem_sec.name} Iy <= 0.0. Changing to 2. {log_fin}")
            if gp.Iz <= 0.0:
                gp.Iz = 2
                logging.error(f"Section {self.fem_sec.name} Iz <= 0.0. Changing to 2. {log_fin}")
            if gp.Iyz <= 0.0:
                gp.Iyz = (gp.Iy + gp.Iz) / 2
                logging.error(f"Section {self.fem_sec.name} Iyz <= 0.0. Changing to (Iy + Iz) / 2. {log_fin}")
            if gp.Iy * gp.Iz - gp.Iyz ** 2 < 0:
                old_y = str(gp.Iy)
                gp.Iy = 1.1 * (gp.Iy + (gp.Iyz ** 2) / gp.Iz)
                logging.error(
                    f"Warning! Section {self.fem_sec.name}: I(11)*I(22)-I(12)**2 MUST BE POSITIVE. "
                    f"Mod Iy={old_y} to {gp.Iy}. {log_fin}"
                )
            if (-(gp.Iy + gp.Iz) / 2 < gp.Iyz <= (gp.Iy + gp.Iz) / 2) is False:
                raise ValueError("Iyz must be between -(Iy+Iz)/2 and (Iy+Iz)/2")
            return f"{gp.Ax}, {gp.Iy}, {gp.Iyz}, {gp.Iz}, {gp.Ix}\n {n1}\n {mat.E:.3E}, ,{mat.alpha:.2E}"
        elif self.section_data == "PIPE":
            return f"{self.fem_sec.section.r}, {self.fem_sec.section.wt}\n {n1}"
        elif self.section_data == "L":
            return (
                f"{self.fem_sec.section.w_btn}, {self.fem_sec.section.h}, {self.fem_sec.section.t_fbtn}, "
                f"{self.fem_sec.section.t_w}\n {n1}"
            )
        else:
            raise NotImplementedError(f'section type "{self.fem_sec.section.type}" is not added to Abaqus export yet')

    @property
    def beam_str(self):
        """
        BOX, CIRC, HEX, I, L, PIPE, RECT, THICK PIPE, and TRAPEZOID sections
        https://abaqus-docs.mit.edu/2017/English/SIMACAEKEYRefMap/simakey-r-beamsection.htm


        General Section
        https://abaqus-docs.mit.edu/2017/English/SIMACAEKEYRefMap/simakey-r-beamgeneralsection.htm#simakey-r-beamgeneralsection__simakey-r-beamgeneralsection-s-datadesc1


        Comment regarding Rotary Inertia and Explicit analysis
        https://abaqus-docs.mit.edu/2017/English/SIMACAEELMRefMap/simaelm-c-beamsectionbehavior.htm#hj-top

        """
        top_line = f"** Section: {self.fem_sec.elset.name}  Profile: {self.fem_sec.elset.name}"
        density = self.fem_sec.material.model.rho if self.fem_sec.material.model.rho > 0.0 else 1e-4
        ass = self.fem_sec.parent.parent.get_assembly()

        rotary_str = ""
        if len(ass.fem.steps) > 0:
            initial_step = ass.fem.steps[0]
            if initial_step.type == "explicit":
                rotary_str = ", ROTARY INERTIA=ISOTROPIC"

        if self.section_data != "GENERAL":
            return (
                f"{top_line}\n*Beam Section, elset={self.fem_sec.elset.name}, material={self.fem_sec.material.name}, "
                f"temperature={self._temp_str}, section={self.section_data}{rotary_str}\n{self.props}"
            )
        else:
            return f"""{top_line}
*Beam General Section, elset={self.fem_sec.elset.name}, section=GENERAL{rotary_str}, density={density}
 {self.props}"""

    @property
    def shell_str(self):
        return f"""** Section: {self.fem_sec.name}
*Shell Section, elset={self.fem_sec.elset.name}, material={self.fem_sec.material.name}
 {self.fem_sec.thickness}, {self.fem_sec.int_points}"""

    @property
    def solid_str(self):
        return f"""** Section: {self.fem_sec.name}
*Solid Section, elset={self.fem_sec.elset.name}, material={self.fem_sec.material.name}
,"""

    @property
    def str(self):
        if self.fem_sec.type == "solid":
            return self.solid_str
        elif self.fem_sec.type == "shell":
            return self.shell_str
        elif self.fem_sec.type == "beam":
            return self.beam_str


class AbaConstraint:
    """

    Coupling definition:
    https://abaqus-docs.mit.edu/2017/English/SIMACAEKEYRefMap/simakey-r-coupling.htm#simakey-r-coupling

    :type constraint: ada.fem.Constraint
    """

    def __init__(self, constraint, fem_writer):
        self.constraint = constraint
        self._fem_writer = fem_writer

    @property
    def _coupling(self):
        dofs_str = "".join(
            [f" {x[0]}, {x[1]}\n" if type(x) != int else f" {x}, {x}\n" for x in self.constraint.dofs]
        ).rstrip()

        if type(self.constraint.s_set) is FemSet:
            new_surf = surface_str(
                Surface(
                    f"{self.constraint.name}_surf",
                    "NODE",
                    self.constraint.s_set,
                    1.0,
                    parent=self.constraint.s_set.parent,
                ),
                self._fem_writer,
            )
            surface_ref = f"{self.constraint.name}_surf"
            add_str = new_surf
        else:
            add_str = "**"
            surface_ref = get_instance_name(self.constraint.s_set, self._fem_writer)

        if self.constraint.csys is not None:
            new_csys_str = "\n" + csys_str(self.constraint.csys, self._fem_writer)
            cstr = f", Orientation={self.constraint.csys.name.upper()}"
        else:
            cstr = ""
            new_csys_str = ""

        rnode = f"{get_instance_name(self.constraint.m_set.members[0], self._fem_writer)}"
        return f"""** ----------------------------------------------------------------
** Coupling element {self.constraint.name}
** ----------------------------------------------------------------{new_csys_str}
** COUPLING {self.constraint.name}
{add_str}
*COUPLING, CONSTRAINT NAME={self.constraint.name}, REF NODE={rnode}, SURFACE={surface_ref}{cstr}
*KINEMATIC
{dofs_str}""".rstrip()

    @property
    def _mpc(self):
        mpc_type = self.constraint.mpc_type
        m_members = self.constraint.m_set.members
        s_members = self.constraint.s_set.members
        mpc_vars = "\n".join([f" {mpc_type},{m.id:>8},{s.id:>8}" for m, s in zip(m_members, s_members)])
        return f"** Constraint: {self.constraint.name}\n*MPC\n{mpc_vars}"

    @property
    def _shell2solid(self):
        mname = self.constraint.m_set.name
        sname = self.constraint.s_set.name

        return (
            f"** Constraint: {self.constraint.name}\n*Shell to Solid Coupling, "
            f"constraint name={self.constraint.name}\n{mname}, {sname}"
        )

    @property
    def str(self):
        if self.constraint.type == "coupling":
            return self._coupling
        elif self.constraint.type == "tie":
            return _tie(self.constraint)
        elif self.constraint.type == "rigid body":
            rnode = get_instance_name(self.constraint.m_set, self)
            return f"*Rigid Body, ref node={rnode}, elset={get_instance_name(self.constraint.s_set, self)}"
        elif self.constraint.type == "mpc":
            return self._mpc
        elif self.constraint.type == "shell2solid":
            return self._shell2solid
        else:
            raise NotImplementedError(f"{self.constraint.type}")


def _tie(constraint):
    """

    :param constraint:
    :type constraint: ada.fem.Constraint
    :return: Constraint string
    """
    num = 80
    pos_tol_str = ""
    if constraint.pos_tol is not None:
        pos_tol_str = f", position tolerance={constraint.pos_tol},"

    coupl_text = "**" + num * "-" + """\n** COUPLING {}\n""".format(constraint.name) + "**" + num * "-" + "\n"
    name = constraint.name

    adjust = constraint.metadata.get("adjust", "no")

    coupl_text += f"""** Constraint: {name}
*Tie, name={name}, adjust={adjust}{pos_tol_str}
{constraint.m_set.name}, {constraint.s_set.name}"""
    return coupl_text


def aba_write(el):
    """

    :type el: ada.fem.Elem
    """
    nl = NewLine(10, suffix=7 * " ")
    if len(el.nodes) > 6:
        di = " {}"
    else:
        di = "{:>13}"
    return f"{el.id:>7}, " + " ".join([f"{di.format(no.id)}," + next(nl) for no in el.nodes])[:-1]


def elwriter(eltype_set, elements):
    if "connector" in eltype_set:
        return None
    eltype, elset = eltype_set
    el_set_str = f", ELSET={elset.name}" if elset is not None else ""
    el_str = "\n".join(map(aba_write, elements))
    return f"""*ELEMENT, type={eltype}{el_set_str}\n{el_str}\n"""


def interaction_str(interaction, fem_writer):
    """

    :param interaction:
    :param fem_writer:
    :type interaction: ada.fem.Interaction
    """
    # Allowing Free text to be parsed directly through interaction class.
    if "aba_bulk" in interaction.metadata.keys():
        return interaction.metadata["aba_bulk"]

    contact_mod = interaction.metadata["contact_mod"] if "contact_mod" in interaction.metadata.keys() else "NEW"
    contact_incl = (
        interaction.metadata["contact_inclusions"]
        if "contact_inclusions" in interaction.metadata.keys()
        else "ALL EXTERIOR"
    )

    top_str = f"**\n** Interaction: {interaction.name}"
    if interaction.type == "SURFACE":
        adjust_par = interaction.metadata.get("adjust", None)
        geometric_correction = interaction.metadata.get("geometric_correction", None)
        small_sliding = interaction.metadata.get("small_sliding", None)

        first_line = "" if small_sliding is None else f", {small_sliding}"

        if type(interaction.parent) is Step:
            step = interaction.parent
            assert isinstance(step, Step)
            first_line += "" if "explicit" in step.type else f", type={interaction.surface_type}"
        else:
            first_line += f", type={interaction.surface_type}"

        if interaction.constraint is not None:
            first_line += f", mechanical constraint={interaction.constraint}"

        if adjust_par is not None:
            first_line += f", adjust={adjust_par}" if adjust_par is not None else ""

        if geometric_correction is not None:
            first_line += f", geometric correction={geometric_correction}"

        return f"""{top_str}
*Contact Pair, interaction={interaction.interaction_property.name}{first_line}
{get_instance_name(interaction.surf1, fem_writer)}, {get_instance_name(interaction.surf2, fem_writer)}"""
    else:
        return f"""{top_str}\n*Contact, op={contact_mod}
*Contact Inclusions, {contact_incl}
*Contact Property Assignment
 ,  , {interaction.interaction_property.name}"""


def material_str(material):
    """

    :param material:
    :type material: ada.Material
    :return:
    """

    if "aba_inp" in material.metadata.keys():
        return material.metadata["aba_inp"]
    if "rayleigh_damping" in material.metadata.keys():
        alpha, beta = material.metadata["rayleigh_damping"]
    else:
        alpha, beta = None, None

    no_compression = material.metadata["no_compression"] if "no_compression" in material.metadata.keys() else False
    compr_str = "\n*No Compression" if no_compression is True else ""

    if material.model.eps_p is not None and len(material.model.eps_p) != 0:
        pl_str = "\n*Plastic\n"
        pl_str += "\n".join(
            ["{x:>12.5E}, {y:>10}".format(x=x, y=y) for x, y in zip(material.model.sig_p, material.model.eps_p)]
        )
    else:
        pl_str = ""

    if alpha is not None and beta is not None:
        d_str = "\n*Damping, alpha={alpha}, beta={beta}".format(alpha=material.model.alpha, beta=material.model.beta)
    else:
        d_str = ""

    if material.model.zeta is not None and material.model.zeta != 0.0:
        exp_str = "\n*Expansion\n {zeta}".format(zeta=material.model.zeta)
    else:
        exp_str = ""

    # Density == 0.0 is unsupported
    density = material.model.rho if material.model.rho > 0.0 else 1e-6

    return f"""*Material, name={material.name}
*Elastic
{material.model.E:.6E},  {material.model.v}{compr_str}
*Density
{density},{exp_str}{d_str}{pl_str}"""


def amplitude_str(amplitude):
    """
    Create an amplitude string for abaqus input-files

    :param amplitude:
    :type amplitude: ada.fem.Amplitude
    """
    name, x, y, smooth = amplitude.name, amplitude.x, amplitude.y, amplitude.smooth
    a = 1
    data = ""
    for i, var in enumerate(zip(list(x), list(y))):
        if a == 4:
            if i == len(list(x)) - 1:
                data += "{:.4E}, {:.4E}, ".format(var[0], var[1])
            else:
                data += "{:.4E}, {:.4E},\n         ".format(var[0], var[1])
            a = 0
        else:
            data += "{:.4E}, {:.4E}, ".format(var[0], var[1])
        a += 1

    smooth = ", DEFINITION=TABULAR, SMOOTH={}".format(smooth) if smooth is not None else ""
    amplitude = """*Amplitude, name={0}{2}\n         {1}\n""".format(name, data, smooth)
    return amplitude.rstrip()


def connector_str(connector, fem_writer):
    """

    :param connector:
    :param fem_writer:
    :type connector: ada.fem.Connector
    :return:
    """

    csys_ref = "" if connector.csys is None else f'\n "{connector.csys.name}",'

    end1 = get_instance_name(connector.n1, fem_writer)
    end2 = get_instance_name(connector.n2, fem_writer)
    return f"""**
** ----------------------------------------------------------------
** Connector element representing {connector.name}
** ----------------------------------------------------------------
**
*Elset, elset={connector.name}
 {connector.id},
*Element, type=CONN3D2
 {connector.id}, {end1}, {end2}
*Connector Section, elset={connector.name}, behavior={connector.con_sec.name}
 {connector.con_type},{csys_ref}
*Elset, elset={connector.name}_set
 {connector.id}
**
{csys_str(connector.csys, fem_writer)}
**"""


def connector_section_str(con_sec):
    """

    :param con_sec:
    :type con_sec: ada.fem.ConnectorSection
    :return:
    """

    conn_txt = """*Connector Behavior, name={0}""".format(con_sec.name)
    elast = con_sec.elastic_comp
    damping = con_sec.damping_comp
    plastic_comp = con_sec.plastic_comp
    rigid_dofs = con_sec.rigid_dofs
    soft_elastic_dofs = con_sec.soft_elastic_dofs
    if type(elast) is float:
        conn_txt += """\n*Connector Elasticity, component=1\n{0:.3E},""".format(elast)
    else:
        for i, comp in enumerate(elast):
            if isinstance(comp, Iterable) is False:
                conn_txt += """\n*Connector Elasticity, component={1} \n{0:.3E},""".format(comp, i + 1)
            else:
                conn_txt += f"\n*Connector Elasticity, nonlinear, component={i + 1}, DEPENDENCIES=1"
                for val in comp:
                    conn_txt += "\n" + ", ".join([f"{x:>12.3E}" if u <= 1 else f",{x:>12d}" for u, x in enumerate(val)])

    if type(damping) is float:
        conn_txt += """\n*Connector Damping, component=1\n{0:.3E},""".format(damping)
    else:
        for i, comp in enumerate(damping):
            if type(comp) is float:
                conn_txt += """\n*Connector Damping, component={1} \n{0:.3E},""".format(comp, i + 1)
            else:
                conn_txt += """\n*Connector Damping, nonlinear, component=1, DEPENDENCIES=1"""
                for val in comp:
                    conn_txt += "\n" + ", ".join(
                        ["{:>12.3E}".format(x) if u <= 1 else ",{:>12d}".format(x) for u, x in enumerate(val)]
                    )

    # Optional Choices
    if plastic_comp is not None:
        for i, comp in enumerate(plastic_comp):
            conn_txt += """\n*Connector Plasticity, component={}\n*Connector Hardening, definition=TABULAR""".format(
                i + 1
            )
            for val in comp:
                force, motion, rate = val
                conn_txt += "\n{}, {}, {}".format(force, motion, rate)

    if rigid_dofs is not None:
        conn_txt += "\n*Connector Elasticity, rigid\n "
        conn_txt += ", ".join(["{0}".format(x) for x in rigid_dofs])

    if soft_elastic_dofs is not None:
        for dof in soft_elastic_dofs:
            conn_txt += "\n*Connector Elasticity, component={0}\n 5.0,\n".format(dof)

    return conn_txt


def interaction_prop_str(int_prop):
    """

    :param int_prop:
    :type int_prop: ada.fem.InteractionProperty
    :return:
    """
    iprop_str = f"*Surface Interaction, name={int_prop.name}\n"

    # Friction
    iprop_str += f"*Friction\n{int_prop.friction},\n"

    # Behaviours
    tab_str = (
        "\n" + "\n".join(["{:>12.3E},{:>12.3E}".format(d[0], d[1]) for d in int_prop.tabular])
        if int_prop.tabular is not None
        else ""
    )
    iprop_str += f"*Surface Behavior, pressure-overclosure={int_prop.pressure_overclosure}{tab_str}"

    return iprop_str.rstrip()


def surface_str(surface, fem_writer):
    """

    :param surface:
    :param fem_writer:
    :type surface: ada.fem.Surface
    :return:
    """
    top_line = f"*Surface, type={surface.type}, name={surface.name}"

    if surface.id_refs is None:
        if surface.type == "NODE":
            add_str = surface.weight_factor
        else:
            add_str = surface.face_id_label
        # if surface.fem_set.name in surface.parent.elsets.keys():
        #     return f"{top_line}\n{surface.fem_set.name}, {add_str}"
        # else:
        return f"""{top_line}\n{get_instance_name(surface.fem_set, fem_writer)}, {add_str}"""
    else:
        id_refs_str = "\n".join([f"{m[0]}, {m[1]}" for m in surface.id_refs]).strip()
        return f"""{top_line}\n{id_refs_str}"""


def bc_str(bc, fem_writer):
    """

    :param bc:
    :param fem_writer:
    :type bc: ada.fem.Bc
    :return:
    """
    ampl_ref_str = "" if bc.amplitude_name is None else ", amplitude=" + bc.amplitude_name
    fem_set = bc.fem_set
    inst_name = get_instance_name(fem_set, fem_writer)

    if bc.type in _valid_aba_bcs:
        aba_type = bc.type
    else:
        aba_type = _aba_bc_map[bc.type]

    dofs_str = ""
    for dof, magn in zip(bc.dofs, bc.magnitudes):
        if dof is None:
            continue
        magn_str = f", {magn:.6E}" if magn is not None else ""
        if bc.type in ["connector displacement", "connector velocity"] or type(dof) is str:
            dofs_str += f" {inst_name}, {dof}{magn_str}\n"
        else:
            dofs_str += f" {inst_name}, {dof}, {dof}{magn_str}\n"

    dofs_str = dofs_str.rstrip()
    add_map = {
        "connector displacement": ("*Connector Motion", ", type=DISPLACEMENT"),
        "connector velocity": ("*Connector Motion", ", type=VELOCITY"),
        "other": ("*Boundary", ""),
    }

    if bc.type in add_map.keys():
        bcstr, add_str = add_map[bc.type]
    else:
        bcstr, add_str = add_map["other"]

    return f"""** Name: {bc.name} Type: {aba_type}
{bcstr}{ampl_ref_str}{add_str}
{dofs_str}"""


def mass_str(mass):
    """

    :param mass:
    :type mass: ada.fem.Mass
    :return:
    """

    type_str = f", type={mass.point_mass_type}" if mass.point_mass_type is not None else ""
    mstr = ",".join([str(x) for x in mass.mass]) if type(mass.mass) is list else str(mass.mass)

    if mass.type == "MASS":
        return f"""*Mass, elset={mass.fem_set.name}{type_str}\n {mstr}"""
    elif mass.type == "NONSTRUCTURAL MASS":
        return f"""*Nonstructural Mass, elset={mass.fem_set.name}, units={mass.units}\n  {mstr}"""
    elif mass.type == "ROTARY INERTIA":
        return f"""*Rotary Inertia, elset={mass.fem_set.name}\n  {mstr}"""
    else:
        raise ValueError(f'Mass type "{mass.type}" is not supported by Abaqus')


def load_str(load):
    """

    :param load:
    :type load: ada.fem.Load
    :return:
    """

    if load.type == "gravity":
        dof = [0, 0, 1] if load.dof is None else load.dof
        dof_str = ", ".join([str(x) for x in dof[:3]])
        return f"""** Name: gravity   Type: Gravity\n*Dload\n, GRAV, {load.magnitude}, {dof_str}"""
    elif load.type == "force":
        lstr = ""
        bc_text_f = ""
        bc_text_m = ""

        fo = 0
        instance_name = get_instance_name(load.fem_set, Load)
        for i, bc in enumerate(load.dof[:3]):
            if bc == 0.0 or bc is None:
                continue
            bc_text_f += f" {instance_name}, {i + 1}, {bc}\n"
            fo += 1

        mom = 0
        for i, bc in enumerate(load.dof[3:]):
            if bc == 0.0 or bc is None:
                continue
            mom += 1
            bc_text_m += f" {instance_name}, {i+4}, {bc}\n"

        lstr += "\n" if "\n" not in lstr[-2:] != "" else ""
        follower_str = "" if load.follower_force is False else ", follower"
        follower_str += ", amplitude={}".format(load.amplitude) if load.amplitude is not None else ""
        if fo != 0:
            lstr += "** Name: {}   Type: Concentrated force\n*Cload{}\n{}".format(
                load.name + "_F", follower_str, bc_text_f
            )
        if mom != 0:
            lstr += "** Name: {}   Type: Moment\n*Cload{}\n{}".format(load.name + "_M", follower_str, bc_text_m)
        return lstr.strip()
    else:
        raise ValueError("Unsupported load type", load.type)


def get_instance_name(obj, fem_writer):
    """

    :param obj:
    :param fem_writer:
    :return:
    """
    from ada import Assembly, Node

    if type(obj) is Node:
        obj_ref = obj.id
        if type(obj.parent.parent) is Assembly:
            assembly_level = True
        else:
            assembly_level = False
    else:
        obj_ref = obj.name
        assembly_level = obj.on_assembly_level

    if fem_writer in (Step, Load) and assembly_level is False:
        return f"{obj.parent.instance_name}.{obj_ref}"
    elif type(obj.parent.parent) != Assembly and type(fem_writer) in (AbaqusWriter, AbaConstraint, AbaStep):
        return f"{obj.parent.instance_name}.{obj_ref}"
    else:
        return str(obj_ref)


def aba_set_str(aba_set, fem_writer=None):
    """

    :param aba_set:
    :param fem_writer: Which level is the set str written at
    :type aba_set: FemSet
    :return:
    """
    if len(aba_set.members) == 0:
        if "generate" in aba_set.metadata.keys():
            if aba_set.metadata["generate"] is False:
                raise ValueError(f'set "{aba_set.name}" is empty. Please check your input')
        else:
            raise ValueError("No Members are found")

    generate = aba_set.metadata.get("generate", False)
    internal = aba_set.metadata.get("internal", False)
    newline = NewLine(15)

    el_str = "*Elset, elset" if aba_set.type == "elset" else "*Nset, nset"

    el_instances = dict()

    for parent, mem in groupby(aba_set.members, key=attrgetter("parent")):
        el_instances[parent.name] = list(mem)

    set_str = ""
    for elinst, members in el_instances.items():
        el_root = f"{el_str}={aba_set.name}"
        if type(fem_writer) in (AbaqusWriter, AbaStep):
            if internal is True:
                el_root += "" if "," in el_str[-2] else ", "
                el_root += "internal"
            if elinst != aba_set.parent.name:
                el_root += "" if "," in el_str[-2] else ", "
                el_root += f"instance={elinst}"

        if generate is True:
            assert len(aba_set.metadata["gen_mem"]) == 3
            el_root += "" if "," in el_root[-2] else ", "
            set_str += (
                el_root + "generate\n {},  {},   {}" "".format(*[no for no in aba_set.metadata["gen_mem"]]) + "\n"
            )
        else:
            set_str += el_root + "\n " + " ".join([f"{no.id}," + next(newline) for no in members]).rstrip()[:-1] + "\n"
    return set_str.rstrip()


def orientations_str(assembly, fem_writer):
    """
    Add orientations associated with loads


    :param assembly:
    :param fem_writer:
    :type assembly: ada.Assembly
    """
    cstr = "** Orientations associated with Loads"
    for step in assembly.fem.steps:
        for load in step.loads:
            if load.csys is not None:
                cstr += "\n"
                coord_str = ", ".join([str(x) for x in chain.from_iterable(load.csys.coords)])[:-1]
                name = load.fem_set.name
                inst_name = get_instance_name(load.fem_set, fem_writer)
                cstr += f"*Nset, nset=_T-{name}, internal\n{inst_name},\n"
                cstr += f"*Transform, nset=_T-{name}\n{coord_str}\n"
                cstr += csys_str(load.csys, fem_writer)

    return cstr.strip()


def csys_str(csys, fem_writer):
    """

    :param csys:
    :param fem_writer:
    :type csys: ada.fem.Csys
    :return:
    """

    name = csys.name.replace('"', "").upper()
    ori_str = f'*Orientation, name="{name}"'
    if csys.nodes is None and csys.coords is None:
        ori_str += "\n 1.,           0.,           0.,           0.,           1.,           0.\n 1, 0."
    elif csys.nodes is not None:
        assert len(csys.nodes) == 3
        ori_str += ", SYSTEM=RECTANGULAR, DEFINITION=NODES\n {},{},{}".format(
            *[get_instance_name(no, fem_writer) for no in csys.nodes]
        )
    else:
        ax, ay, az = csys.coords[0]
        ori_str += f" \n{ax}, {ay}, {az}"
        bx, by, bz = csys.coords[1]
        ori_str += f", {bx}, {by}, {bz}"
        if len(csys.coords) == 3:
            cx, cy, cz = csys.coords[2]
            ori_str += f", {cx}, {cy}, {cz}"
        ori_str += "\n 1, 0."
    return ori_str


def hist_output_str(hist_output):
    """

    :param hist_output:
    :type hist_output: ada.fem.HistOutput
    :return:
    """
    hist_map = dict(
        connector="*Element Output, elset=",
        node="*Node Output, nset=",
        energy="*Energy Output",
        contact="*Contact Output",
    )

    if hist_output.type not in hist_map.keys():
        raise Exception('Unknown output type "{}"'.format(hist_output.type))

    set_type_str = hist_map[hist_output.type]
    newline = NewLine(10)
    var_str = "".join([" {},".format(val) + next(newline) for val in hist_output.variables])[:-1]

    if hist_output.type == "contact":
        iname1 = get_instance_name(hist_output.fem_set[1], Step)
        iname2 = get_instance_name(hist_output.fem_set[0], Step)
        fem_set_str = f", master={iname1}, slave={iname2}"
    else:
        fem_set_str = "" if hist_output.fem_set is None else get_instance_name(hist_output.fem_set, Step)
    return f"""*Output, history, {hist_output.int_type}={hist_output.int_value}
** HISTORY OUTPUT: {hist_output.name}
**
{set_type_str}{fem_set_str}
{var_str}"""


def field_output_str(field_output):
    """

    :param field_output:
    :type field_output: ada.fem.FieldOutput
    :return:
    """
    if len(field_output.nodal) > 0:
        nodal_str = "*Node Output\n "
        nodal_str += ", ".join([str(val) for val in field_output.nodal])
    else:
        nodal_str = "** No Nodal Output"

    if len(field_output.element) > 0:
        element_str = "*Element Output, directions=YES\n "
        element_str += ", ".join([str(val) for val in field_output.element])
    else:
        element_str = "** No Element Output"

    if len(field_output.contact) > 0:
        contact_str = "*Contact Output\n "
        contact_str += ", ".join([str(val) for val in field_output.contact])
    else:
        contact_str = "** No Contact Output"
    return f"""** FIELD OUTPUT: {field_output.name}
**
*Output, field, {field_output.int_type}={field_output.int_value}
{nodal_str}
{element_str}
{contact_str}""".strip()


def add_freq_range(fmin, fmax, intervals=100):
    """
    Return a multiline string of frequency range given by <fmin> and <fmax> at a specific interval.

    :param intervals: Number of intervals for frequency range. Default is 100.
    :param fmin: Minimum frequency
    :param fmax: Maximum frequency
    :return:
    """
    freq_list = np.linspace(fmin, fmax, intervals)
    freq_str = ""
    for eig in freq_list:
        if eig == freq_list[-1]:
            print("last one")
            freq_str += "{0:.3f},".format(eig)
        else:
            freq_str += "{0:.3f},\n".format(eig)

    return freq_str


def predefined_field_str(pre_field):
    """

    :param pre_field:
    :type pre_field: ada.fem.PredefinedField
    :return:
    """
    dofs_str = ""
    for dof, magn in zip(pre_field.dofs, pre_field.magnitude):
        if float(magn) == 0.0:
            continue
        dofs_str += f"{get_instance_name(pre_field.fem_set, pre_field)}, {dof}, {magn}\n"
    dofs_str.rstrip()
    return f"""** PREDEFINED FIELDS
**
** Name: {pre_field.name}   Type: {pre_field.type}
*Initial Conditions, type={pre_field.type}
{dofs_str}"""


def spring_str(spring):
    """

    :param spring:
    :type spring: Spring
    :return:
    """
    if spring.type == "SPRING1":
        _str = f'** Spring El "{spring.name}"\n\n'
        for dof, row in enumerate(spring.stiff):
            for j, stiffness in enumerate(row):
                if dof == j:
                    _str += f"""*Spring, elset={spring.fem_set.name}
 {dof + 1}
 {stiffness:.6E}
{spring.id}, {spring.nodes[0].id}\n"""
        return _str.rstrip()
    else:
        raise ValueError(f'Currently unsupported spring type "{spring.type}"')
