from o3seespy.base_model import OpenSeesObject, OpenSeesMultiCallObject, opy
from o3seespy.opensees_instance import OpenSeesInstance
from o3seespy.exceptions import ModelError


def set_node_mass(osi, node, x_mass, y_mass, rot_mass):
    op_type = 'mass'
    parameters = [node.tag, x_mass, y_mass, rot_mass]
    osi.to_process(op_type, parameters)


class Mass(OpenSeesObject):
    op_base_type = "mass"
    op_type = None

    def __init__(self, osi, node, x_mass, y_mass=None, rot_mass=None):
        if osi.ndf > 2 and rot_mass is None:
            rot_mass = 0.0
        self.node = node
        self.x_mass = x_mass
        self.y_mass = y_mass
        self.rot_mass = rot_mass
        self._parameters = [self.node.tag, self.x_mass]
        if self.y_mass is not None:
            self._parameters.append(self.y_mass)
        if self.rot_mass is not None:
            if self.y_mass is None:
                self._parameters.append(0.0)
            self._parameters.append(self.rot_mass)
        self.to_process(osi)
        

class Mass2D(OpenSeesObject):
    op_base_type = "mass"
    op_type = None

    def __init__(self, osi, node, x_mass, y_mass=None, rot_mass=None):
        if osi.ndf > 2 and rot_mass is None:
            rot_mass = 0.0
        self.node = node
        self.x_mass = x_mass
        self.y_mass = y_mass
        self.rot_mass = rot_mass
        self._parameters = [self.node.tag, self.x_mass]
        if self.y_mass is not None:
            self._parameters.append(self.y_mass)
        if self.rot_mass is not None:
            if self.y_mass is None:
                self._parameters.append(0.0)
            self._parameters.append(self.rot_mass)
        self.to_process(osi)
        
        
class Mass3D(OpenSeesObject):
    op_base_type = "mass"
    op_type = None

    def __init__(self, osi, node, x, y, z, x_rot, y_rot, z_rot):
        self.node = node
        self.x = x
        self.y = y
        self.z = y
        self.x_rot = x_rot
        self.y_rot = y_rot
        self.z_rot = z_rot
        self._parameters = [self.node.tag, self.x, self.y, self.z, self.x_rot, self.y_rot, self.z_rot]
        self.to_process(osi)


def set_equal_dof(osi, node_1, node_2, dof):
    op_type = 'equalDOF'
    parameters = [node_1.tag, node_2.tag,  dof]
    osi.to_process(op_type, parameters)


def set_equal_dofs(osi, node_1, node_2, dofs):
    op_type = 'equalDOF'
    parameters = [node_1.tag, node_2.tag,  *dofs]
    osi.to_process(op_type, parameters)


def set_equal_dofs_mixed(osi, node_1, node_2, num_dof, rcdofs):  # TODO: validate
    op_type = 'equalDOF_Mixed'
    parameters = [node_1.tag, node_2.tag,  num_dof, *rcdofs]
    osi.to_process(op_type, parameters)


class EqualDOF(OpenSeesObject):
    op_base_type = "equalDOF"
    op_type = None

    def __init__(self, osi, r_node, c_node, dofs):
        """
        Construct a constraint where the constrained node `c_node`
         has the same movement as the retained node `r_node`

        :param osi:
        :param r_node: OpenSeesObject.node.Node() or list of
            Retained node
        :param c_node: OpenSeesObject.node.Node() or list of
            Constrained node
        :param dofs:
        """
        self.r_node = r_node
        self.c_node = c_node
        self.dofs = dofs
        self._parameters = [self.r_node.tag, self.c_node.tag, *self.dofs]
        self.to_process(osi)


class EqualDOFMulti(OpenSeesMultiCallObject):
    op_base_type = "equalDOF"
    op_type = None

    def __init__(self, osi, r_node, c_nodes, dofs):
        """
        Construct a constraint where the c_node has the same movement as the r_node

        :param osi:
        :param r_node: OpenSeesObject.node.Node() or list of
            Retained node
        :param c_nodes: list of OpenSeesObject.node.Node()
            Constrained node
        :param dofs:
        """
        self.r_node = r_node
        self.c_nodes = c_nodes
        self.dofs = dofs
        if hasattr(r_node, '__len__'):
            r_nodes = self.r_node
        else:
            r_nodes = [self.r_node for i in range(len(self.c_nodes))]
        self._multi_parameters = []
        for i in range(len(c_nodes)):
            self._multi_parameters.append([r_nodes[i].tag, self.c_nodes[i].tag, *self.dofs])
            self.to_process(osi)


class ModalDamping(OpenSeesObject):
    """
    ModalDamping class

    Notes:
        * Need to run eigen() first, do not use 'fullGenLapack' option in eigen analysis
        * Cannot be used with Newmark_Explicit, but works with other explicit and implicit methods
        * Creates a full damping matrix, therefore either use solver.FullGen (very slow), or sparse solvers
         like UmfPack (when doing this using algorithm.KrylovNewton or algorithm.BFGS, not algorithm.NewtonRaphson,
         see https://portwooddigital.com/2019/09/12/be-careful-with-modal-damping/ for more details)
    """
    op_base_type = "modalDamping"
    op_type = None

    def __init__(self, osi, xis):
        """

        :param osi:
        :param xis: array_like
            List of damping values at each mode, or just provide first value in list.
        """

        self.xis = xis
        self._parameters = self.xis
        self.to_process(osi)


def set_rigid_diaphragm(osi, r_node, cnodes, perp_dir):
    cnode_tags = [x.tag for x in cnodes]
    op_type = 'rigidDiaphragm'
    parameters = [perp_dir, r_node.tag,  *cnode_tags]
    osi.to_process(op_type, parameters)


def set_rigid_link(osi, r_node, c_node, rtype):
    """
    Create a multi-point constraint between nodes.

    Parameters
    ----------
    r_node: OpenSeesObject.node.Node()
        Retained node
    c_node: Node
        Constrained node
    rtype: str
        Either 'bar' or 'beam'

    Returns
    -------

    """
    op_type = 'rigidLink'
    parameters = [rtype, r_node.tag, c_node.tag]
    osi.to_process(op_type, parameters)


class RigidLink(OpenSeesObject):
    op_base_type = 'rigidLink'
    op_type = None
    def __init__(self, osi, rtype, r_node, c_node):
        """
        Create a multi-point constraint between nodes.

        Parameters
        ----------
        rtype: str
            Either 'bar' or 'beam'
        r_node: OpenSeesObject.node.Node()
            Retained node
        c_node: Node
            Constrained node

        Returns
        -------

        """
        self.rtype = rtype
        self.r_node = r_node
        self.c_node = c_node
        self._parameters = [rtype, r_node.tag, c_node.tag]
        self.to_process(osi)


class Fix1DOF(OpenSeesObject):
    op_base_type = "fix"
    op_type = None

    def __init__(self, osi, node, x):
        """
        Create a homogeneous SP constraint.

        Parameters
        ----------
        osi: OpenSeesInstance
        node: OpenSeesObject.node.Node()
        x: int
            Fixity in x-direction
        """
        self.node = node
        self.x = x
        self._parameters = [self.node.tag, self.x]
        self.to_process(osi)


class Fix1DOFMulti(OpenSeesMultiCallObject):
    op_base_type = "fix"
    op_type = None

    def __init__(self, osi, nodes, x, is_none='raise'):
        """
        Create a homogeneous SP constraint.

        Parameters
        ----------
        osi: OpenSeesInstance
        nodes: list of OpenSeesObject.node.Node()
        x: int
            Fixity in x-direction
        """
        self.nodes = nodes
        self.x = x
        self._multi_parameters = []
        for node in self.nodes:
            try:
                self._multi_parameters.append([node.tag, self.x])
                self.to_process(osi)
            except AttributeError as e:
                if is_none == 'raise':
                    raise e
                else:
                    pass


class Fix2DOF(OpenSeesObject):
    op_base_type = "fix"
    op_type = None

    def __init__(self, osi, node, x, y):
        """
        Create a homogeneous SP constraint.

        Parameters
        ----------
        osi: OpenSeesInstance
        node: OpenSeesObject.node.Node()
        x: int
            Fixity in x-direction
        y: int
            Fixity in y-direction
        """
        self.node = node
        self.x = x
        self.y = y
        self._parameters = [self.node.tag, self.x, self.y]
        self.to_process(osi)


class Fix2DOFMulti(OpenSeesMultiCallObject):
    op_base_type = "fix"
    op_type = None

    def __init__(self, osi, nodes, x, y, is_none='raise', already_fixed='raise'):
        """
        Create a homogeneous SP constraint.

        Parameters
        ----------
        osi: OpenSeesInstance
        nodes: list of OpenSeesObject.node.Node()
        x: int
            Fixity in x-direction
        y: int
            Fixity in y-direction
        """
        self.nodes = nodes
        self.x = x
        self.y = y
        self._multi_parameters = []
        for node in self.nodes:
            try:
                self._multi_parameters.append([node.tag, self.x, self.y])
                self.to_process(osi)
            except AttributeError as e:
                if is_none == 'raise':
                    raise e
                else:
                    pass
            except ValueError as e:
                if already_fixed == 'raise':
                    raise e
                else:
                    pass
                

def add_fixity_to_dof(osi, dof, nodes):
    if osi.ndf == 1:
        fn = Fix1DOF
        arr = [0]
    elif osi.ndf == 2:
        fn = Fix2DOF
        arr = [0, 0]
    elif osi.ndf == 3:
        fn = Fix3DOF
        arr = [0, 0, 0]
    elif osi.ndf == 6:
        fn = Fix6DOF
        arr = [0, 0, 0, 0, 0, 0]
    else:
        raise ModelError("'create_fixities_for_dof' only supports ndf=1,2,3,6")
    arr[dof - 1] = 1
    for node in nodes:
        if node is None:
            continue
        try:
            fn(osi, node, *arr)
        except ValueError:
            if osi.state == 3:
                osi.commands = osi.commands[:-1]
            pass

# 
# class Fix2DOaFMulti(OpenSeesMultiCallObject):
#     op_base_type = "fix"
#     op_type = None
# 
#     def __init__(self, osi, nodes, x, y):
#         """
#         Create a homogeneous SP constraint.
# 
#         Parameters
#         ----------
#         osi: OpenSeesInstance
#         nodes: list of OpenSeesObject.node.Node()
#         x: int
#             Fixity in x-direction
#         y: int
#             Fixity in y-direction
#         """
#         self.nodes = nodes
#         self.x = x
#         self.y = y
#         self._multi_parameters = []
#         for node in self.nodes:
#             self._multi_parameters.append([node.tag, self.x, self.y])
#             self.to_process(osi)
        

class Fix3DOF(OpenSeesObject):
    op_base_type = "fix"
    op_type = None

    def __init__(self, osi, node, x, y, z_rot):
        """
        Create a homogeneous SP constraint.

        Parameters
        ----------
        osi: OpenSeesInstance
        node: OpenSeesObject.node.Node()
        x: int
            Fixity in x-direction
        y: int
            Fixity in y-direction
        z_rot: int
            Fixity in rotation about z-axis
        """
        self.node = node
        self.x = x
        self.y = y
        self.z_rot = z_rot
        self._parameters = [self.node.tag, self.x, self.y, self.z_rot]
        self.to_process(osi)


class Fix3DOFMulti(OpenSeesMultiCallObject):
    op_base_type = "fix"
    op_type = None

    def __init__(self, osi, nodes, x, y, z_rot, is_none='raise', already_fixed='raise'):
        """
        Create a homogeneous SP constraint.

        Parameters
        ----------
        osi: OpenSeesInstance
        nodes: list of OpenSeesObject.node.Node()
        x: int
            Fixity in x-direction
        y: int
            Fixity in y-direction
        z_rot: int
            Fixity in rotation about z-axis
        """
        self.nodes = nodes
        self.x = x
        self.y = y
        self.z_rot = z_rot
        self._multi_parameters = []
        for node in self.nodes:
            try:
                self._multi_parameters.append([node.tag, self.x, self.y, self.z_rot])
                self.to_process(osi)
            except AttributeError as e:
                if is_none == 'raise':
                    raise e
                else:
                    pass
            except ValueError as e:
                if already_fixed == 'raise':
                    raise e
                else:
                    pass


class Fix4DOF(OpenSeesObject):
    op_base_type = "fix"
    op_type = None

    def __init__(self, osi, node, x, y, z, pp):
        """
        Create a homogeneous SP constraint.

        Parameters
        ----------
        osi: OpenSeesInstance
        node: OpenSeesObject.node.Node()
        x: int
            Fixity in x-direction
        y: int
            Fixity in y-direction
        z: int
            Fixity in z-direction
        pp: int
            Fixity in pore-pressure
        """
        self.node = node
        self.x = x
        self.y = y
        self.z = z
        self.pp = pp
        self._parameters = [self.node.tag, self.x, self.y, self.z, self.pp]
        self.to_process(osi)


class Fix6DOF(OpenSeesObject):
    op_base_type = "fix"
    op_type = None

    def __init__(self, osi, node, x, y, z, x_rot, y_rot, z_rot):
        """
        Create a homogeneous SP constraint.

        Parameters
        ----------
        osi: OpenSeesInstance
        node: OpenSeesObject.node.Node()
        x: int
            Fixity in x-direction
        y: int
            Fixity in y-direction
        z: int
            Fixity in z-direction
        x_rot: int
            Fixity in rotation about x-axis
        y_rot: int
            Fixity in rotation about y-axis
        z_rot: int
            Fixity in rotation about z-axis
        """
        self.node = node
        self.x = x
        self.y = y
        self.z = z
        self.x_rot = x_rot
        self.y_rot = y_rot
        self.z_rot = z_rot
        self._parameters = [self.node.tag, self.x, self.y, self.z, self.x_rot, self.y_rot, self.z_rot]
        self.to_process(osi)


class Fix6DOFMulti(OpenSeesMultiCallObject):
    op_base_type = "fix"
    op_type = None

    def __init__(self, osi, nodes, x, y, z, x_rot, y_rot, z_rot, is_none='raise'):
        """
        Create a homogeneous SP constraint.

        Parameters
        ----------
        osi: OpenSeesInstance
        nodes: list of OpenSeesObject.node.Node()
        x: int
            Fixity in x-direction
        y: int
            Fixity in y-direction
        z: int
            Fixity in z-direction
        x_rot: int
            Fixity in rotation about x-axis
        y_rot: int
            Fixity in rotation about y-axis
        z_rot: int
            Fixity in rotation about z-axis
        """
        self.nodes = nodes
        self.x = x
        self.y = y
        self.z = z
        self.x_rot = x_rot
        self.y_rot = y_rot
        self.z_rot = z_rot
        self._multi_parameters = []
        for node in self.nodes:
            try:
                self._multi_parameters.append([node.tag, self.x, self.y, self.z, self.x_rot, self. y_rot, self.z_rot])
                self.to_process(osi)
            except AttributeError as e:
                if is_none == 'raise':
                    raise e
                else:
                    pass


class Fix(OpenSeesObject):
    op_base_type = "fix"
    op_type = None

    def __init__(self, osi, node, fixities):
        """
        Create a homogeneous SP constraint.

        """
        self.node = node
        self.fixities = fixities
        self._parameters = [self.node.tag, *self.fixities]
        self.to_process(osi)


class Load(OpenSeesObject):
    op_base_type = "load"
    op_type = None

    def __init__(self, osi, node, load_values):
        self.node = node
        self.load_values = load_values

        self._parameters = [self.node.tag, *self.load_values]
        self.to_process(osi)


class EleLoad2DPoint(OpenSeesObject):
    op_base_type = "eleLoad"
    op_type = None

    def __init__(self, osi, ele, p_y, x, p_x=None):
        """
        Type of load is 'beamPoint'

        x: float
            Position of load as a fraction of element length from node i
        """
        self.ele_tag = ele.tag
        self.x = float(x)
        self.p_y = float(p_y)
        self.p_x = p_x

        self._parameters = ['-ele', self.ele_tag, '-type', '-beamPoint', self.p_y, self.x]
        if self.p_x is not None:
            self._parameters.append(float(self.p_x))
        self.to_process(osi)


class EleLoad2DUniform(OpenSeesObject):
    op_base_type = "eleLoad"
    op_type = None

    def __init__(self, osi, ele, w_y, w_x=None):
        """
        Type of load is 'beamUniform'
        """
        self.ele_tag = ele.tag
        self.w_y = float(w_y)
        self.w_x = w_x

        self._parameters = ['-ele', self.ele_tag, '-type', '-beamUniform', self.w_y]
        if self.w_x is not None:
            self._parameters.append(float(self.w_x))
        self.to_process(osi)


class SP(OpenSeesObject):
    op_base_type = "sp"
    op_type = None

    def __init__(self, osi, node, dof, dof_values):
        self.node = node
        self.dof = int(dof)
        self.dof_values = dof_values

        self._parameters = [self.node.tag, self.dof, *self.dof_values]
        self.to_process(osi)


class ImposedMotion(OpenSeesObject):
    op_base_type = "ImposedMotion"
    op_type = None

    def __init__(self, osi, node, ory, g_motion_tag):
        """

        """
        self.node = node
        self.ory = ory
        self.g_motion_tag = g_motion_tag
        self._parameters = [self.node.tag, self.ory, self.g_motion_tag]
        self.to_process(osi)


def analyze(osi, num_inc=1, dt=None, dt_min=None, dt_max=None, jd=None):
    """
    Performs an analysis step.

    Returns 0 if successful, and <0 if fail

    Parameters
    ----------
    osi: o3.OpenSeesInstance
    num_inc: int
        Number of analysis increments
    dt: float
        Time step
    dt_min: float
        (Only used in VariableTransient analsyis) Minimum allowable time step
    dt_max: float
        (Only used in VariableTransient analsyis) Maximum allowable time step
    jd: int
        (Only used in VariableTransient analsyis) Target number of integration steps.
        In variable transient analysis, time step is scaled by n_iters / jd, and analysis
        continues until total time increment >= num_inc * dt.

    Returns
    -------

    """
    op_type = 'analyze'
    if dt is None:
        parameters = [int(num_inc)]
    elif dt_min is None:
        parameters = [int(num_inc), float(dt)]
    else:
        parameters = [int(num_inc), float(dt), dt_min, dt_max, jd]
    return osi.to_process(op_type, parameters)


def loop_for_analyze_w_restart(osi, num_inc=1, dt=None, dt_min=None, dt_max=None, jd=None, alts=None, dtime=None):
    if osi.logfile_name is None:
        raise ValueError('cannot find logfile, use osi.set_logfile')
    if len(alts) == 0:
        return 1  # failed
    init_time = get_time(osi)
    curr_alt = alts[0]
    # alt: (dt, alg)
    if dtime is None:
        dtime = num_inc * dt
    prev_dt = dt
    if curr_alt[0] is not None:
        dt = curr_alt[0]

    num_inc = int(dtime / dt)
    fail = analyze(osi, num_inc=num_inc, dt=dt, dt_min=dt_min, dt_max=dt_max, jd=jd)
    if fail:
        curr_time = get_time(osi)
        dtime = curr_time - init_time
        if dtime <= 0:
            return 0
        alts = alts[1:]
        fail = loop_for_analyze_w_restart(osi, num_inc=num_inc, dt=dt, dt_min=dt_min, dt_max=dt_max, jd=jd,
                                      alts=alts, dtime=dtime)
    return 0


class AlternativeAnalysis(object):
    def __init__(self, dt=None):
        self.dt = dt
        pass

def analyze_w_restart(osi, num_inc=1, dt=None, dt_min=None, dt_max=None, jd=None, alts=None, dtime=1, nfs=1):
    if osi.logfile_name is None:
        raise ValueError('cannot find logfile, use osi.set_logfile')
    if alts is None:
        alts = []
    init_time = get_time(osi)
    tot_time = num_inc * dt
    for i in range(nfs):
        curr_loc_time = get_time(osi) - init_time
        if curr_loc_time >= tot_time:
            break
        num_inc = int(tot_time - curr_loc_time / dt)
        fail = analyze(osi, num_inc=num_inc, dt=dt, dt_min=dt_min, dt_max=dt_max, jd=jd)
        if fail:
            curr_loc_time = get_time(osi) - init_time
            if curr_loc_time >= tot_time:
                break
            num_inc = int(tot_time - curr_loc_time / dt)
            fail = loop_for_analyze_w_restart(osi, num_inc=num_inc, dt=dt, dt_min=dt_min, dt_max=dt_max, jd=jd,
                                              alts=alts, dtime=dtime)
        else:
            break

        pass
        # get curr time.


def get_node_disp(osi, node, dof=None, node_as_tag=False):
    op_type = 'nodeDisp'
    if node_as_tag:
        parameters = [node]
    else:
        parameters = [node.tag]
    if dof is not None:
        parameters.append(dof)
    return osi.to_process(op_type, parameters)


def get_node_disps(osi, node, dofs, node_as_tag=False):
    op_type = 'nodeDisp'
    vals = []
    for dof in dofs:
        if node_as_tag:
            parameters = [node, dof]
        else:
            parameters = [node.tag, dof]
        vals.append(osi.to_process(op_type, parameters))
    return vals


def get_all_node_disps(osi, dof):
    tags = get_node_tags(osi)
    disps = []
    for tag in tags:
        disps.append(osi.to_process('nodeDisp', [tag, dof]))  # very slow
    return disps


def get_node_vel(osi, node, dof):
    op_type = 'nodeVel'
    parameters = [node.tag, dof]
    return osi.to_process(op_type, parameters)


def get_node_accel(osi, node, dof):
    op_type = 'nodeAccel'
    parameters = [node.tag, dof]
    return osi.to_process(op_type, parameters)


def gen_reactions(osi):
    op_type = 'reactions'
    parameters = []
    return osi.to_process(op_type, parameters)


def get_node_reaction(osi, node, dof):
    op_type = 'nodeReaction'
    parameters = [node.tag, dof]
    return osi.to_process(op_type, parameters)


def get_node_reactions(osi, node):
    op_type = 'nodeReaction'
    parameters = [node.tag]
    return osi.to_process(op_type, parameters)


def get_node_unbalance(osi, node, dof=None):
    op_type = 'nodeUnbalance'
    parameters = [node.tag]
    if dof is not None:
        parameters.append(dof)
    return osi.to_process(op_type, parameters)


def get_node_pressure(osi, node):
    op_type = 'nodePressure'
    parameters = [node.tag, 2]
    return osi.to_process(op_type, parameters)


def get_ele_response(osi, ele, arg, extra_args=None):
    params = [ele.tag, arg]
    if extra_args is not None:
        params = [ele.tag, *extra_args, arg]
    return osi.to_process('eleResponse', params)


def remove_sp(osi, node, dof, pattern=None):
    op_type = 'remove'
    parameters = ['sp', node.tag, dof]
    if pattern is not None:
        parameters.append(pattern.tag)
    # p_str = ', '.join([str(x) for x in parameters])
    return osi.to_process(op_type, parameters)


def remove_load_pattern(osi, load_pattern):
    op_type = 'remove'
    parameters = ['loadPattern', load_pattern.tag]
    return osi.to_process(op_type, parameters)


def remove(osi, o3_obj):
    """Generic remover"""
    op_type = 'remove'
    parameters = [o3_obj.op_base_type, o3_obj.tag]
    return osi.to_process(op_type, parameters)


def remove_recorders(osi):
    """Remove all recorders"""
    op_type = 'remove'
    return osi.to_process(op_type, ['recorders'])


def set_parameter(osi, value, eles=None, ele_range=None, args=None, ele_tag_range=None):
    """Set a parameter on an element"""
    op_type = 'setParameter'
    parameters = ['-val', value]
    if eles is not None:
        ele_tags = [x.tag for x in eles]
        parameters += ['-ele', *ele_tags]
    elif ele_range is not None:
        ele_tags = [x.tag for x in ele_range]
        parameters += ['-eleRange', *ele_tags]
    elif ele_tag_range is not None:
        parameters += ['-eleRange', *ele_tag_range]
    # else:
    #     raise ValueError("'eles or ele_range must not be None in set_parameter")
    if args:
        parameters += [str(x) for x in args]
    else:
        raise ValueError("'args' can not be None in set_parameter")
    # p_str = ', '.join([str(x) for x in parameters])
    return osi.to_process(op_type, parameters)


def set_time(osi, time):
    """Reset the analysis time to a new value"""
    osi.to_process('setTime', [time])


def get_time(osi):
    """Get the analysis time"""
    return osi.to_process('getTime', [])


def wipe_analysis(osi):
    osi.to_process('wipeAnalysis', [])


def wipe(osi=None):
    """Wipe the current analysis and save the results to file"""
    if osi is None:
        return opy.wipe()
    osi.to_process('wipe', [])

def reset(osi):
    """Reset the current analysis and save the results to file"""
    osi.to_process('reset', [])


def record(osi):
    """This command is used to cause all the recorders to do a record on the current state of the model."""
    return osi.to_process('record', [])


def load_constant(osi, time=None):
    params = []
    if time is not None:
        params += ['-time', time]
    osi.to_process('loadConst', params)


def update_material_stage(osi, material, stage):
    parameters = ['-material', material.tag, '-stage', stage]
    osi.to_process("updateMaterialStage", parameters)


def get_eigen(osi, solver='genBandArpack', n=1):
    """Gets a list eigen values"""
    parameters = [f'-{solver}', n]
    outs = osi.to_process("eigen", parameters)
    if not hasattr(outs, '__len__'):
        return [outs]
    return outs

def get_node_eigen_vector(osi, node, eigen_vector, dof):
    parameters = [node.tag, eigen_vector, dof]
    outs = osi.to_process("nodeEigenvector", parameters)
    return outs


def get_pid(osi):
    """Get the processor ID of the calling processor."""
    return osi.to_process('getPID', [])


def get_np(osi):
    """Get total number of processors."""
    return osi.to_process('getNP', [])


def set_num_threads(osi, num):
    """Set the total number of threads"""
    return osi.to_process('setNumThread', [num])


def get_num_threads(osi):
    """return the total number of threads available"""
    return osi.to_process('getNumThread', [])


def get_node_dofs(osi, node):
    """Returns the DOF numbering of a node."""
    return osi.to_process('nodeDOFs', node.tag)


def get_node_tags(osi, mesh=None):
    """Returns the OpenSEES numbering of the nodes."""
    params = []
    if mesh is not None:
        params += ['-mesh', mesh.tag]
    return osi.to_process('getNodeTags', params)


def get_ele_tags(osi, mesh=None):
    """Returns the OpenSEES numbering of the elements."""
    params = []
    if mesh is not None:
        params += ['-mesh', mesh.tag]
    return osi.to_process('getEleTags', params)


def get_node_coords(osi, node, ndm=None, node_as_tag=False):
    if node_as_tag:
        tag = node
    else:
        tag = node.tag
    if ndm is not None:
        pms = [ndm]
    else:
        pms = []
    return osi.to_process('nodeCoord', [tag, *pms])


def get_all_node_coords(osi, ndm=None):
    tags = get_node_tags(osi)
    coords = []
    if ndm is not None:
        pms = [ndm]
    else:
        pms = []
    for tag in tags:
        coords.append(osi.to_process('nodeCoord', [tag, *pms]))  # very slow
    return coords


def get_all_node_coords_w_tag(osi, ndm=None):
    tags = get_node_tags(osi)
    coords = []
    if ndm is not None:
        pms = [ndm]
    else:
        pms = []
    for tag in tags:
        coords.append([tag, *osi.to_process('nodeCoord', [tag, *pms])])  # very slow
    return coords


def get_all_ele_node_tags(osi):
    ele_tags = get_ele_tags(osi)
    node_tags = []
    for tag in ele_tags:
        node_tags.append(osi.to_process('eleNodes', [tag]))
    return node_tags


def get_all_ele_node_tags_as_dict(osi):
    ele_tags = get_ele_tags(osi)
    if not hasattr(ele_tags, '__len__'):
        ele_tags = [ele_tags]
    node_tags = {}
    for ele_tag in ele_tags:
        node_tags[ele_tag] = osi.to_process('eleNodes', [ele_tag])
    return node_tags


def get_all_ele_node_tags_by_n_nodes(osi):
    ele_tags = get_ele_tags(osi)
    all_node_tags = {}
    for ele_tag in ele_tags:
        node_tags = osi.to_process('eleNodes', [ele_tag])
        if node_tags is not None:
            if len(node_tags) not in all_node_tags:
                all_node_tags[len(node_tags)] = {}
            all_node_tags[len(node_tags)][ele_tag] = node_tags
    return all_node_tags


class Parameter(OpenSeesObject):
    op_base_type = 'parameter'
    op_type = None

    def __init__(self, osi, ele, section=None, mat=None, pname=''):
        """

        Parameters
        ----------
        osi
        ele
        section
        mat
        pname
        """
        self.ele = ele
        self.section = section
        self.mat = mat
        self.pname = pname
        osi.n_params += 1
        self._tag = osi.n_params
        self._parameters = [self.tag, 'element', self.ele.tag]
        if self.section is not None:
            self._parameters += ['section', self.section]  # unsure if this is correct
        if self.mat is not None:
            self._parameters += ['material', self.mat]
        self._parameters.append(pname)
        self.to_process(osi)


def update_parameter(osi, param, value):
    """Set a parameter to a new value"""
    op_type = 'updateParameter'
    parameters = [param.tag, value]
    return osi.to_process(op_type, parameters)


def start(osi):
    """Start the timer"""
    return osi.to_process('start', [])

def stop(osi):
    """Stop the timer and print timing information."""
    return osi.to_process('stop', [])


def update_element_domain(osi):
    """Update elements in the domain"""
    return osi.to_process('updateElementDomain', [])

def domain_change(osi):
    return osi.to_process('domainChange', [])


def remesh(osi, alpha):
    return osi.to_process('remesh', [alpha])


def set_element_rayleigh_damping_factors(osi, ele, alpha_m, beta_k, beta_k0, beta_kc):
    return osi.to_process('setElementRayleighDampingFactors', [ele.tag, alpha_m, beta_k, beta_k0, beta_kc])


def set_ele_rayleigh_damp(osi, ele, alpha_m, beta_k, beta_k0, beta_kc):
    return osi.to_process('setElementRayleighDampingFactors', [ele.tag, alpha_m, beta_k, beta_k0, beta_kc])


def imposed_motion(osi, node, direct, gm):
    return osi.to_process('imposedMotion', [node.tag, direct, gm.tag])
