import numpy as np

class Laminate(object):

    def __init__(self, lamID = None, stacking_sequence = None, offset = None):
        """Initialize the Laminate object
        
        Parameters
        ----------

        lamID : str
            Laminate id (required)

        stacking_sequence : [ply.Ply, ..., ply.Ply]
            List of ply starting from the bottom of the laminate 
            (at -laminate_thickness/2.0) and ending at the top of the laminate
            (at +laminate_thickness/2.0), i.e., full stacking sequence.

        offset: float (required)
            Reference plane offset from midplane
            t/2 : top
            0.0 : mid
            -t/2 : bottom
        
        """

        self.lamID = str(lamID)
        self.stacking_sequence = stacking_sequence
        self.offsetKey = str(offset).lower()
        self._invalidate()

    @classmethod
    def fromDict(cls, **kwargs):
        """Initialize the Laminate object from dictionary.
        
        Parameters
        ----------

        kwargs : dict
            {"lamID": str, "stacking_sequence": [Ply, Ply, ..., Ply]}

        Returns
        -------

        cls : Laminate
        
        """

        lamID = kwargs.get("lamID")
        stacking_sequence = kwargs.get("stacking_sequence")
        offset = kwargs.get("offset")
        return cls(lamID, stacking_sequence, offset)

    def _invalidate(self):
        """Invalidates the computed attributes to force recalculation."""

        self._A = None
        self._B = None
        self._Bmid = None
        self._D = None
        self._Dmid = None
        self._thickness = None
        self._offset = None
        self._z = None
        self._zmid = None
        self._ABD = None
        self._gamma = None
        self._delta = None
        self._A44 = None
        self._A45 = None
        self._A55 = None
        self._rho = None
        self._Ex = None
        self._Ey = None
        self._Gxy = None
        self._vxy = None
        self._vyx = None
        self._Efx = None
        self._Efy = None
        self._Gfxy = None
        self._vfxy = None
        self._vfyx = None
        self._alphax = None
        self._alphay = None
        self._alphaxy = None
        self._betax = None
        self._betay = None
        self._betaxy = None

    @property
    def offset(self):
        """Laminate offset."""

        if self._offset is not None:
            return self._offset

        if self.offsetKey == "top":
            self._offset = self.thickness/2.0
        elif self.offsetKey == "bottom":
            self._offset = -self.thickness/2.0
        else:
            self._offset = 0.0

        return self._offset

    @property
    def thickness(self):
        """Total laminate thickness."""

        if self._thickness is None:
            self._thickness = 0.0
            for ply in self.stacking_sequence:
                self._thickness += ply.thickness
        return self._thickness

    @property
    def rho(self):
        """Average laminate density using rule of mixtures."""

        if self._rho is None:
            self._rho = 0.0
            for ply in self.stacking_sequence:
                self._rho += ply.material.rho*ply.thickness
            self._rho = self._rho/self.thickness
        return self._rho

    @property
    def z(self):
        """List of locations starting from bottom of the laminate
        at every ply level.

        Returns
        -------
        z : [-t/2-offset, ..., 0-offset, ..., t/2-offset]
        
        """

        if self._z is None:
            self._z = [(-self.thickness/2.0)-self.offset]
            for ply in self.stacking_sequence:
                val = self._z[-1]+ply.thickness
                self._z.append(val)
        return self._z

    @property
    def zmid(self):
        """List of locations starting from bottom of the laminate
        at every ply level assuming reference plane is at mid plane.

        Returns
        -------
        zmid : [-t/2-offset, ..., 0-offset, ..., t/2-offset]
        
        """

        if self._zmid is None:
            self._zmid = [-self.thickness/2.0]
            for ply in self.stacking_sequence:
                val = self._zmid[-1]+ply.thickness
                self._zmid.append(val)
        return self._zmid

    @property
    def A(self):
        """A Matrix of the laminate
        
        Returns
        -------
        
        A : numpy.array
        
        """

        if self._A is None:
            self._A = np.zeros((3,3))
            for i, ply in enumerate(self.stacking_sequence, start=1):
                self._A += ply.Qbar*(self.z[i]-self.z[i-1])
        return self._A

    @property
    def A44(self):
        """Transverse stiffness of the laminate"""

        if self._A44 is None:
            self._A44 = 0.0
            for i, ply in enumerate(self.stacking_sequence, start=1):
                self._A44 += ply.Q44bar*(self.z[i]-self.z[i-1])
        return self._A44

    @property
    def A45(self):
        """Transverse stiffness of the laminate"""

        if self._A45 is None:
            self._A45 = 0.0
            for i, ply in enumerate(self.stacking_sequence, start=1):
                self._A45 += ply.Q45bar*(self.z[i]-self.z[i-1])
        return self._A45

    @property
    def A55(self):
        """Transverse stiffness of the laminate"""

        if self._A55 is None:
            self._A55 = 0.0
            for i, ply in enumerate(self.stacking_sequence, start=1):
                self._A55 += ply.Q55bar*(self.z[i]-self.z[i-1])
        return self._A55

    @property
    def B(self):
        """B Matrix of the laminate
        
        Returns
        -------
        
        B : numpy.array
        
        """

        if self._B is None:
            self._B = np.zeros((3,3))
            for i, ply in enumerate(self.stacking_sequence, start=1):
                self._B += 0.5*ply.Qbar*((self.z[i]**2)-(self.z[i-1]**2))
        return self._B

    @property
    def Bmid(self):
        """B Matrix of the laminate assuming reference plane is at the 
        mid plane.
        
        Returns
        -------
        
        Bmid : numpy.array
        
        """

        if self._Bmid is None:
            self._Bmid = np.zeros((3,3))
            for i, ply in enumerate(self.stacking_sequence, start=1):
                self._Bmid += 0.5*ply.Qbar*((self.zmid[i]**2)-(self.zmid[i-1]**2))
        return self._Bmid

    @property
    def D(self):
        """D Matrix of the laminate
        
        Returns
        -------
        
        D : numpy.array
        
        """

        if self._D is None:
            self._D = np.zeros((3,3))
            for i, ply in enumerate(self.stacking_sequence, start=1):
                self._D += (1.0/3.0)*ply.Qbar*((self.z[i]**3)-(self.z[i-1]**3))
        return self._D

    @property
    def Dmid(self):
        """D Matrix of the laminate assuming reference plane is at the mid plane.
        
        Returns
        -------
        
        Dmid : numpy.array
        
        """

        if self._Dmid is None:
            self._Dmid = np.zeros((3,3))
            for i, ply in enumerate(self.stacking_sequence, start=1):
                self._Dmid += (1.0/3.0)*ply.Qbar*((self.zmid[i]**3)-(self.zmid[i-1]**3))
        return self._Dmid

    @property
    def ABD(self):
        """Returns full stiffness matrix in the form of 

        [A B]

        [B D]
        
        Returns
        -------
        
        ABD : numpy.array
        
        """

        if self._ABD is None:
            self._ABD = np.zeros((6,6))
            self._ABD[0:3,0:3] = self.A
            self._ABD[0:3,3:6] = self.B
            self._ABD[3:6,0:3] = self.B
            self._ABD[3:6,3:6] = self.D
        return self._ABD

    @property
    def gamma(self):
        """Laminate property measuring the effect of D16. If the computed 
        value is less than 0.1, then D16 can be ignored.
        
        """

        if self._gamma is None:
            D11 = self.Dmid[0, 0]
            D12 = self.Dmid[0, 1]
            D16 = self.Dmid[0, 2]
            D22 = self.Dmid[1, 1]
            D26 = self.Dmid[1, 2]
            D66 = self.Dmid[2, 2]
            self._gamma = D16/(((D11)**3)*D22)**0.25
        return self._gamma

    @property
    def delta(self):
        """Laminate property measuring the effect of D26. If the computed 
        value is less than 0.1, then D26 can be ignored.
        
        """
        
        if self._delta is None:
            D11 = self.Dmid[0, 0]
            D12 = self.Dmid[0, 1]
            D16 = self.Dmid[0, 2]
            D22 = self.Dmid[1, 1]
            D26 = self.Dmid[1, 2]
            D66 = self.Dmid[2, 2]
            self._delta = D26/(((D22)**3)*D11)**0.25
        return self._delta

    @property
    def Ex(self):
        """Laminate engineering constant, Ex = 1/a11*t
        
        """

        if self._Ex is not None:
            return self._Ex

        self._calculateEngineeringConstants()

        return self._Ex

    @property
    def Ey(self):
        """Laminate engineering constant, Ey = 1/a22*t
        
        """

        if self._Ey is not None:
            return self._Ey

        self._calculateEngineeringConstants()

        return self._Ey

    @property
    def Gxy(self):
        """Laminate engineering constant, Gxy = 1/a33*t
        
        """

        if self._Gxy is not None:
            return self._Gxy
        
        self._calculateEngineeringConstants()

        return self._Gxy

    @property
    def vxy(self):
        """Laminate engineering constant, vxy = -a12/a11
        
        """

        if self._vxy is not None:
            return self._vxy

        self._calculateEngineeringConstants()

        return self._vxy

    @property
    def vyx(self):
        """Laminate engineering constant, vyx = -a12/a22
        
        """

        if self._vyx is not None:
            return self._vyx
        
        self._calculateEngineeringConstants()

        return self._vyx

    @property
    def Efx(self):
        """Laminate engineering constant, Efx = 12/d11*t^3
        
        """

        if self._Efx is not None:
            return self._Efx

        self._calculateEngineeringConstants()

        return self._Efx

    @property
    def Efy(self):
        """Laminate engineering constant, Efy = 12/d22*t*3
        
        """

        if self._Efy is not None:
            return self._Efy

        self._calculateEngineeringConstants()

        return self._Efy

    @property
    def Gfxy(self):
        """Laminate engineering constant, Gfxy = 12/d33*t^3
        
        """

        if self._Gfxy is not None:
            return self._Gfxy
        
        self._calculateEngineeringConstants()

        return self._Gfxy

    @property
    def vfxy(self):
        """Laminate engineering constant, vfxy = -d12/d11
        
        """

        if self._vfxy is not None:
            return self._vfxy

        self._calculateEngineeringConstants()

        return self._vfxy

    @property
    def vfyx(self):
        """Laminate engineering constant, vfyx = -d12/d22
        
        """

        if self._vfyx is not None:
            return self._vfyx
        
        self._calculateEngineeringConstants()

        return self._vfyx

    @property
    def alphax(self):
        """Laminate engineering constant, alphax
        
        """

        if self._alphax is not None:
            return self._alphax

        self._calculateEngineeringConstants()

        return self._alphax

    @property
    def alphay(self):
        """Laminate engineering constant, alphay
        
        """

        if self._alphay is not None:
            return self._alphay

        self._calculateEngineeringConstants()

        return self._alphay

    @property
    def alphaxy(self):
        """Laminate engineering constant, alphaxy
        
        """

        if self._alphaxy is not None:
            return self._alphaxy

        self._calculateEngineeringConstants()

        return self._alphaxy

    @property
    def betax(self):
        """Laminate engineering constant, betax
        
        """

        if self._betax is not None:
            return self._betax

        self._calculateEngineeringConstants()

        return self._betax

    @property
    def betay(self):
        """Laminate engineering constant, betay
        
        """

        if self._betay is not None:
            return self._betay

        self._calculateEngineeringConstants()

        return self._betay

    @property
    def betaxy(self):
        """Laminate engineering constant, betaxy
        
        """

        if self._betaxy is not None:
            self._betaxy

        self._calculateEngineeringConstants()

        return self._betaxy

    def _calculateEngineeringConstants(self):
        """Calculates engineering constants
        
        """

        AI = np.linalg.inv(self.A)
        t = self.thickness
        self._Ex = 1.0/(AI[0,0]*t)
        self._Ey = 1.0/(AI[1,1]*t)
        self._Gxy = 1.0/(AI[2,2]*t)
        self._vxy = -AI[0,1]/AI[0,0]
        self._vyx = -AI[0,1]/AI[1,1]
        DI = np.linalg.inv(self.Dmid)
        self._Efx = 12.0/(DI[0,0]*(t**3))
        self._Efy = 12.0/(DI[1,1]*(t**3))
        self._Gfxy = 12.0/(DI[2,2]*(t**3))
        self._vfxy = -DI[0,1]/DI[0,0]
        self._vfyx = -DI[0,1]/DI[1,1]

        _alpha = np.zeros(3)
        _beta = np.zeros(3)
        for i, ply in enumerate(self.stacking_sequence, start=1):
            _alpha += (ply.Qbar @ ply.alphabar) * (self.z[i]-self.z[i-1])
            _beta += (ply.Qbar @ ply.betabar) * (self.z[i]-self.z[i-1])
        _alphax = AI @ _alpha
        _betax = AI @ _beta
        self._alphax = _alphax[0]
        self._alphay = _alphax[1]
        self._alphaxy = _alphax[2]
        self._betax = _betax[0]
        self._betay = _betax[1]
        self._betaxy = _betax[2]
