import json
import sys
import numpy

from material.orthotropic import Orthotropic
from ply.ply import Ply
from laminate.laminate import Laminate
from panel.panel import Panel
from loads.loads import Loads
from lambend.lambend import Lambend
from lambend.__version__ import __version__
numpy.set_printoptions(threshold=sys.maxsize, linewidth=1000)

class LambendIO(object):

    def __init__(self, materialFile=None, plyFile=None, laminateFile=None,
    panelFile=None, loadsFile=None, outputFile=None, number_of_terms=None,
    number_of_points_x=None, number_of_points_y=None,
    output_K="no", output_P="no"):
        """Initializes LambendIO instance

        Parameters
        ----------

        materialFile : str

            Full path filename of material json file (required).

        plyfile : str

            Full path filename of ply json file (required).

        laminateFile : str

            Full path filename of laminate json file (required).

        panelFile : str

            Full path filename of panel json file (required).

        loadsFile : str

            Full path filename of loads json file (required).

        outputFile : str

            Full path filename of output file (required).

        number_of_terms : int

            Number of terms to be used in analysis (required).

        number_of_points_x : int

            Number of points in x direction where outputs are reported (required).

        number_of_points_y : int

            Number of points in y direction where outputs are reported (required).
       
        """

        self.materialFile = materialFile
        self.plyFile = plyFile
        self.laminateFile = laminateFile
        self.panelFile = panelFile
        self.loadsFile = loadsFile
        self.outputFile = outputFile
        self.number_of_terms = number_of_terms
        self.number_of_points_x = number_of_points_x
        self.number_of_points_y = number_of_points_y
        self.output_K = output_K
        self.output_P = output_P

        self._invalidate()

    def _invalidate(self):
        self._materials = {}
        self._plies = {}
        self._laminates = {}
        self._panels = {}
        self._loads = {}
        self._results = []
        self._pairs = []
        self._unpairs = []

    @property
    def materials(self):
        """Returns dictionary of materials read from input file. The key 
        in the dictionary is the material id.

        Returns
        -------

        materialDict : dict

            {Material.matID : Material}
        
        """

        if not self._materials:
            with open(self.materialFile, 'r') as fh:
                jsonData = json.load(fh)
            for materialDict in jsonData["materials"]:
                matID = materialDict["matID"]
                E1 = materialDict["E1"]
                E2 = materialDict["E2"]
                G12 = materialDict["G12"]
                G13 = materialDict["G13"]
                G23 = materialDict["G23"]
                v12 = materialDict["v12"]
                alpha1 = materialDict["alpha1"]
                alpha2 = materialDict["alpha2"]
                beta1 = materialDict["beta1"]
                beta2 = materialDict["beta2"]
                self._materials[matID] = Orthotropic(matID=matID, E1=E1, E2=E2, 
                G12=G12, G13=G13, G23=G23, v12=v12, alpha1=alpha1,
                alpha2=alpha2, beta1=beta1, beta2=beta2)

        return self._materials

    @property
    def plies(self):
        """Returns dictionary of plies read from input file. The key 
        in the dictionary is the ply id.

        Returns
        -------

        plyDict : dict

            {Ply.plyID : Ply, ...}
        
        """

        if not self._plies:
            with open(self.plyFile, 'r') as fh:
                jsonData = json.load(fh)
            for plyDict in jsonData["plies"]:
                plyID = plyDict["plyID"]
                angle = plyDict["angle"]
                material = self.materials[plyDict["material"]]
                thickness = plyDict["thickness"]
                self._plies[plyID] = Ply(plyID=plyID, angle=angle, 
                material=material, thickness=thickness)

        return self._plies
    
    @property
    def laminates(self):
        """Returns dictionary of laminates read from input file. The key 
        in the dictionary is the laminate id.

        Returns
        -------

        laminateDict : dict

            {Laminate.lamID : Laminate, ...}
        
        """
        if not self._laminates:
            with open(self.laminateFile, 'r') as fh:
                jsonData = json.load(fh)
            for laminateDict in jsonData["laminates"]:
                lamID = laminateDict["lamID"]
                stacking_sequence = [self.plies[str(plyID)] for plyID in 
                laminateDict["stacking_sequence"]]
                offset = laminateDict["offset"]
                self._laminates[lamID] = Laminate(lamID=lamID, 
                stacking_sequence=stacking_sequence, offset=offset)

                # Read A, B, D from input file directly
                if "A" in laminateDict:
                    A11, A12, A16, A22, A26, A66, A44, A45, A55 = laminateDict["A"]
                    self._laminates[lamID]._A = numpy.array([[A11, A12, A16], [A12, A22, A26], [A16, A26, A66]])
                    self._laminates[lamID]._A44 = A44
                    self._laminates[lamID]._A45 = A45
                    self._laminates[lamID]._A55 = A55
                if "B" in laminateDict:
                    B11, B12, B16, B22, B26, B66 = laminateDict["B"]
                    self._laminates[lamID]._B = numpy.array([[B11, B12, B16], [B12, B22, B26], [B16, B26, B66]])
                if "D" in laminateDict:
                    D11, D12, D16, D22, D26, D66 = laminateDict["D"]
                    self._laminates[lamID]._D = numpy.array([[D11, D12, D16], [D12, D22, D26], [D16, D26, D66]])

        return self._laminates

    @property
    def panels(self):
        """Returns dictionary of panels read from input file. The key 
        in the dictionary is the panel id.

        Returns
        -------

        panelDict : dict

            {Panel.panelID : Panel, ...}
        
        """
        if not self._panels:
            with open(self.panelFile, 'r') as fh:
                jsonData = json.load(fh)
            for panelDict in jsonData["panels"]:
                panelID = panelDict["panelID"]
                length = panelDict["length"]
                width = panelDict["width"]
                laminate = self.laminates[panelDict["laminate"]]
                boundary_condition = panelDict["boundary_condition"]
                self._panels[panelID] = Panel(panelID=panelID, length=length, width=width,
                laminate=laminate, boundary_condition=boundary_condition)
                self._panels[panelID].number_of_terms = self.number_of_terms

        return self._panels

    @property
    def loads(self):
        """Returns dictionary of loads read from input file. The key
        in the dictionary is the loadscase or loads id
        
        Returns
        -------
        
        loadsDict : dict

            {Loads.loadsID_Panel.panelID : Loads, ...}
            
        """

        if not self._loads:
            with open(self.loadsFile, 'r') as fh:
                jsonData = json.load(fh)
                for loadsDict in jsonData["loads"]:
                    loadsID = loadsDict["loadsID"]
                    panelID = loadsDict["panelID"]
                    loadsType = loadsDict["type"]
                    try:
                        p0 = loadsDict["p0"]
                    except:
                        p0 = 0.0

                    try:
                        P = loadsDict["P"]
                    except:
                        P = 0.0
                    try:
                        xCG = loadsDict["xCG"]
                    except:
                        xCG = 0.0
                    try:
                        yCG = loadsDict["yCG"]
                    except:
                        yCG = 0.0
                    try:
                        xLength = loadsDict["xLength"]
                    except:
                        xLength = 0.0
                    try:
                        yLength = loadsDict["yLength"]
                    except:
                        yLength = 0.0
                    try:
                        Nxx = loadsDict["Nxx"]
                    except:
                        Nxx = 0.0
                    try:
                        Nyy = loadsDict["Nyy"]
                    except:
                        Nyy = 0.0
                    try:
                        Nxy = loadsDict["Nxy"]
                    except:
                        Nxy = 0.0
                    try:
                        deltaT = loadsDict["deltaT"]
                    except:
                        deltaT = 0.0
                    try:
                        deltaC = loadsDict["deltaC"]
                    except:
                        deltaC = 0.0
                    self._loads[loadsID+"_"+panelID] = Loads(loadsID=loadsID, panelID=panelID,
                    p0=p0, P=P, xCG=xCG, yCG=yCG, xLength=xLength, yLength=yLength,
                    loadsType = loadsType,
                    Nxx=Nxx, Nyy=Nyy, Nxy=Nxy, deltaT=deltaT, deltaC=deltaC)

        return self._loads

    @property
    def pairs(self):
        """Returns a list of analysis pairs of (Panel, Loads).
        
        Returns
        -------
        
        pairs : list
            [(Panel, Loads), ...]
            
        """

        if not self._pairs:
            for k, lds in self.loads.items():
                pnl = self.panels[lds.panelID]
                self._pairs.append((pnl, lds))

        
        return self._pairs

    @property
    def results(self):
        """Returns a list of Lambend for each pair of (panel, loads)
        
        Returns
        -------

        solver001List : list
            [Lambend, ...]

        """
        if not self._results:
            for pnl, lds in self.pairs:
                lmb = Lambend(panel=pnl, loads=lds, 
                number_of_terms=self.number_of_terms,
                number_of_points_x=self.number_of_points_x,
                number_of_points_y=self.number_of_points_y)
                self._results.append(lmb)
        return self._results

    def _writeResults(self):
        """Writes output in outputfile"""

        outputData = "3pc-lambend, v"+__version__+"\n\n"
        KData = ""
        PData = ""
        for lmb in self.results:
            postedMat = []
            postedMatTH = []
            outputData +=  "LOADS ID  PANEL ID  \n"
            outputData += "{:<10s}".format(lmb.loads.loadsID)
            outputData += "{:<10s}\n\n".format(lmb.panel.panelID)

            outputData += "PANEL GEOMETRY\n"
            outputData += "LENGTH: {:<14.6e}\n".format(lmb.panel.length)
            outputData += "WIDTH : {:<14.6e}\n\n".format(lmb.panel.width)

            outputData += "LOADS DESCRIPTION\n"
            outputData += "TYPE: {:<20s}\n".format(lmb.loads.type.upper())
            if lmb.loads.type == "uniform pressure" or lmb.loads.type == "sinusoidal pressure" or lmb.loads.type == "hydrostatic pressure in x direction" or lmb.loads.type == "hydrostatic pressure in y direction" or lmb.loads.type == "triangular pressure in x direction" or lmb.loads.type == "triangular pressure in y direction":
                outputData += "p0: {:<14.6e}\n".format(lmb.loads.p0)
            elif lmb.loads.type == "partial pressure":
                outputData += "p0: {:<14.6e}\n".format(lmb.loads.p0)
                outputData += "xCG: {:<14.6e}\n".format(lmb.loads.xCG)
                outputData += "yCG: {:<14.6e}\n".format(lmb.loads.yCG)
                outputData += "xLength: {:<14.6e}\n".format(lmb.loads.xLength)
                outputData += "yLength: {:<14.6e}\n".format(lmb.loads.yLength)
            elif lmb.loads.type == "point load":
                outputData += "P: {:<14.6e}\n".format(lmb.loads.P)
                outputData += "xCG: {:<14.6e}\n".format(lmb.loads.xCG)
                outputData += "yCG: {:<14.6e}\n".format(lmb.loads.yCG)
            outputData += "\n"

            outputData += "ANALYSIS OPTIONS\n"
            outputData += "m = {:<14d}\n".format(lmb.number_of_terms)
            outputData += "n = {:<14d}\n\n".format(lmb.number_of_terms)

            outputData += "OUTPUT OPTIONS\n"
            outputData += "NUMBER OF POINTS IN X DIR: {:<14d}\n".format(lmb.number_of_points_x)
            outputData += "NUMBER OF POINTS IN Y DIR: {:<14d}\n\n".format(lmb.number_of_points_y)

            outputData += "{:^87s}\n".format("MATERIAL PROPERTIES")
            outputData += "ID            E1            E2            G12           G23           G13           v12           \n"
            for ply in lmb.panel.laminate.stacking_sequence:
                if ply.material.matID in postedMat:
                    continue
                postedMat.append(ply.material.matID)
                if len(ply.material.matID) <= 13:
                    outputData += "{:<14s}".format(ply.material.matID)
                else:
                    outputData += "{:<14s}".format(ply.material.matID[0:12])
                outputData += "{:<14.6e}".format(ply.material.E1)
                outputData += "{:<14.6e}".format(ply.material.E2)
                outputData += "{:<14.6e}".format(ply.material.G12)
                outputData += "{:<14.6e}".format(ply.material.G23)
                outputData += "{:<14.6e}".format(ply.material.G13)
                outputData += "{:<14.6e}\n".format(ply.material.v12)
            outputData += "\n"

            outputData += "{:^61s}\n".format("HYGROTHERMAL MATERIAL PROPERTIES")
            outputData += "ID            alpha1        alpha2        beta1         beta2         \n"
            for ply in lmb.panel.laminate.stacking_sequence:
                if ply.material.matID in postedMatTH:
                    continue
                postedMatTH.append(ply.material.matID)
                if len(ply.material.matID) <= 13:
                    outputData += "{:<14s}".format(ply.material.matID)
                else:
                    outputData += "{:<14s}".format(ply.material.matID[0:12])
                outputData += "{:<14.6e}".format(ply.material.alpha1)
                outputData += "{:<14.6e}".format(ply.material.alpha2)
                outputData += "{:<14.6e}".format(ply.material.beta1)
                outputData += "{:<14.6e}\n".format(ply.material.beta2)
            outputData += "\n"

            outputData += "{:^28s}\n".format("LAMINATE GEOMETRY")
            outputData += "STACKING SEQUENCE (PLY ANG): ["
            for i, ply in enumerate(lmb.panel.laminate.stacking_sequence, start=1):
                if i < len(lmb.panel.laminate.stacking_sequence):
                    outputData += "{:<+14.1f}, ".format(ply.angle)
                else:
                    outputData += "{:<+14.1f}]\n".format(ply.angle)
            
            outputData += "STACKING SEQUENCE (PLY MAT): ["
            for i, ply in enumerate(lmb.panel.laminate.stacking_sequence, start=1):
                if i < len(lmb.panel.laminate.stacking_sequence):
                    if len(ply.material.matID) <= 13:
                        outputData += "{:<14s}, ".format(ply.material.matID)
                    else:
                        outputData += "{:<14s}, ".format(ply.material.matID[0:12])
                else:
                    if len(ply.material.matID) <= 13:
                        outputData += "{:<14s}]\n".format(ply.material.matID)
                    else:
                        outputData += "{:<14s}]\n".format(ply.material.matID[0:12])

            outputData += "TOTAL THICKNESS: {:<14.6e}\n".format(lmb.panel.laminate.thickness)
            outputData += "OFFSET: {:<14.6e}\n".format(lmb.panel.laminate.offset)
            outputData += "\n"

            outputData += "{:^42s}\n".format("LAMINATE PROPERTIES")
            outputData += "{:^42s}\n".format("A MATRIX")
            outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}\n".format(lmb.panel.laminate.A[0,0], 
            lmb.panel.laminate.A[0,1], lmb.panel.laminate.A[0,2])
            outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}\n".format(lmb.panel.laminate.A[1,0], 
            lmb.panel.laminate.A[1,1], lmb.panel.laminate.A[1,2])
            outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}\n\n".format(lmb.panel.laminate.A[2,0], 
            lmb.panel.laminate.A[2,1], lmb.panel.laminate.A[2,2])

            outputData += "{:^42s}\n".format("A MATRIX - TRANSVERSE SHEAR")
            outputData += "{:<+14.6e}{:<+14.6e}\n".format(lmb.panel.laminate.A44, 
            lmb.panel.laminate.A45)
            outputData += "{:<+14.6e}{:<+14.6e}\n\n".format(lmb.panel.laminate.A45, 
            lmb.panel.laminate.A55)

            outputData += "{:^42s}\n".format("B MATRIX")
            outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}\n".format(lmb.panel.laminate.B[0,0], lmb.panel.laminate.B[0,1], 
            lmb.panel.laminate.B[0,2])
            outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}\n".format(lmb.panel.laminate.B[1,0], lmb.panel.laminate.B[1,1], 
            lmb.panel.laminate.B[1,2])
            outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}\n\n".format(lmb.panel.laminate.B[2,0], lmb.panel.laminate.B[2,1], 
            lmb.panel.laminate.B[2,2])

            if lmb.panel.laminate.offsetKey.upper() != "MIDDLE":
                outputData += "{:^42s}\n".format("B MATRIX - NO OFFSET")
                outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}\n".format(lmb.panel.laminate.Bmid[0,0], lmb.panel.laminate.Bmid[0,1], 
                lmb.panel.laminate.Bmid[0,2])
                outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}\n".format(lmb.panel.laminate.Bmid[1,0], lmb.panel.laminate.Bmid[1,1], 
                lmb.panel.laminate.Bmid[1,2])
                outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}\n\n".format(lmb.panel.laminate.Bmid[2,0], lmb.panel.laminate.Bmid[2,1], 
                lmb.panel.laminate.Bmid[2,2])

            outputData += "{:^42s}\n".format("D MATRIX")
            outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}\n".format(lmb.panel.laminate.D[0,0], lmb.panel.laminate.D[0,1], 
            lmb.panel.laminate.D[0,2])
            outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}\n".format(lmb.panel.laminate.D[1,0], lmb.panel.laminate.D[1,1], 
            lmb.panel.laminate.D[1,2])
            outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}\n\n".format(lmb.panel.laminate.D[2,0], lmb.panel.laminate.D[2,1], 
            lmb.panel.laminate.D[2,2])

            if lmb.panel.laminate.offsetKey.upper() != "MIDDLE":
                outputData += "{:^42s}\n".format("D MATRIX - NO OFFSET")
                outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}\n".format(lmb.panel.laminate.Dmid[0,0], lmb.panel.laminate.Dmid[0,1], 
                lmb.panel.laminate.Dmid[0,2])
                outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}\n".format(lmb.panel.laminate.Dmid[1,0], lmb.panel.laminate.Dmid[1,1], 
                lmb.panel.laminate.Dmid[1,2])
                outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}\n\n".format(lmb.panel.laminate.Dmid[2,0], lmb.panel.laminate.Dmid[2,1], 
                lmb.panel.laminate.Dmid[2,2])

            outputData += "{:^130s}\n".format("LAMINATE INPLANE AND FLEXURAL ENGINEERING CONSTANTS")
            outputData += "Ex            Ey            Gxy           vxy           vyx           "
            outputData += "Efx           Efy           Gfxy          vfxy          vfyx          \n"
            outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}{:<+14.6e}{:<+14.6e}{:<+14.6e}{:<+14.6e}{:<+14.6e}{:<+14.6e}{:<+14.6e}\n\n".format(lmb.panel.laminate.Ex,
            lmb.panel.laminate.Ey, lmb.panel.laminate.Gxy, lmb.panel.laminate.vxy, lmb.panel.laminate.vyx,
            lmb.panel.laminate.Efx, lmb.panel.laminate.Efy, lmb.panel.laminate.Gfxy, lmb.panel.laminate.vfxy,
            lmb.panel.laminate.vfyx)

            outputData += "{:^76s}\n".format("EFFECTIVE LAMINATE HYGROTHERMAL ENGINEERING CONSTANTS")
            outputData += "alphax        alphay        alphaxy       betax         betay         betaxy        \n"
            outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}{:<+14.6e}{:<+14.6e}{:<+14.6e}\n\n".format(lmb.panel.laminate.alphax, 
            lmb.panel.laminate.alphay, lmb.panel.laminate.alphaxy, lmb.panel.laminate.betax, lmb.panel.laminate.betay, lmb.panel.laminate.betaxy)

            outputData += "{:^31s}\n".format("MECHANICAL LOADS")
            outputData += "NX            NY            NXY           \n"
            outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}\n\n".format(lmb.loads.Nxx, lmb.loads.Nyy, lmb.loads.Nxy)

            outputData += "{:^46s}\n".format("THERMAL LOADS")
            outputData += "DELTAT        NXT           NYT           NXYT          \n"
            outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}{:<+14.6e}\n\n".format(lmb.loads.deltaT, lmb.NT[0], lmb.NT[1], lmb.NT[2])

            outputData += "{:^46s}\n".format("HYGRAL LOADS")
            outputData += "DELTAC        NXH           NYH           NXYH          \n"
            outputData += "{:<+14.6e}{:<+14.6e}{:<+14.6e}{:<+14.6e}\n\n".format(lmb.loads.deltaC, lmb.NC[0], lmb.NC[1], lmb.NC[2])

            outputData += "{:^199s}\n".format("GRID POINTS, DISPLACEMENTS, ROTATIONS, FORCES, AND MOMENTS")
            outputData += "X             Y             U             V             W             PHIX          PHIY          "
            outputData += "NXX           NYY           NXY           MXX           MYY           MXY           QYZ           QXZ           \n"
            for loc, uloc, vloc, wloc, phixloc, phiyloc, NMBloc, QBloc in zip(lmb.loc, lmb.uloc, lmb.vloc, lmb.wloc, lmb.phixloc, lmb.phiyloc, lmb.NMBloc, lmb.QBloc):
                outputData += "{:<14.6e}".format(loc[0])
                outputData += "{:<14.6e}".format(loc[1])
                outputData += "{:<+14.6e}".format(uloc)
                outputData += "{:<+14.6e}".format(vloc)
                outputData += "{:<+14.6e}".format(wloc)
                outputData += "{:<+14.6e}".format(phixloc)
                outputData += "{:<+14.6e}".format(phiyloc)
                outputData += "{:<+14.6e}".format(NMBloc[0])
                outputData += "{:<+14.6e}".format(NMBloc[1])
                outputData += "{:<+14.6e}".format(NMBloc[2])
                outputData += "{:<+14.6e}".format(NMBloc[3])
                outputData += "{:<+14.6e}".format(NMBloc[4])
                outputData += "{:<+14.6e}".format(NMBloc[5])
                outputData += "{:<+14.6e}".format(QBloc[0])
                outputData += "{:<+14.6e}\n".format(QBloc[1])
            
            outputData += "\n"

            if self.output_K == "yes":
                KData += f"LOADS ID: {lmb.loads.loadsID} PANEL ID: {lmb.panel.panelID}\n\n"
                KData += f"{lmb.KFull}\n\n"

            if self.output_P == "yes":
                PData += f"LOADS ID: {lmb.loads.loadsID} PANEL ID: {lmb.panel.panelID}\n\n"
                PData += f"{lmb.PFull}\n\n"

        with open(self.outputFile, 'w') as fh:
            fh.write(outputData)

        if self.output_K == "yes":
            with open("K.txt", 'w') as fh:
                fh.write(KData)

        if self.output_P == "yes":
            with open("P.txt", 'w') as fh:
                fh.write(PData)
        