# coding: utf-8
#
# Copyright © 2020 MSC.Software Corporation
#  
# Permission to use Scripts
#  
# Permission is granted to MSC.Software customers with valid licenses of the MSC Apex™ software 
# to use script files posted on this page (collectively “Scripts”) solely for customers’ own authorized 
# internal use in conjunction with licensed use of the MSC Apex software.  
# No other use is permitted.  No license to MSC Apex or other software is granted under this Permission.
#  
# SCRIPTS ARE PROVIDED ON AN "AS-IS" BASIS, WITH NO WARRANTIES OF ANY KIND.  USE OF THE SCRIPTS IS AT CUSTOMER'S SOLE RISK.
#  
# IN NO EVENT SHALL MSC.SOFTWARE CORPORATION, ITS AFFILIATES, SUBSIDIARIES, SUPPLIERS, AGENTS, REPRESENTATIVES OR EMPLOYEES BE LIABLE FOR ANY 
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, TORT, 
# STRICT LIABILITY, OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION USE OF THE SCRIPTS, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  
#  
# Customer shall indemnify, defend and hold MSC.Software Corporation, its affiliates, subsidiaries, suppliers, agents, 
# employees or representatives (collectively, “Indemnified Parties”) harmless from and against any third-party claims 
# brought against Indemnified Parties rising out of or in connection with Customer’s use of the Scripts.
#

import apex
import csv
import math


def readFile(fspec):
    datafile = open(fspec, 'r')
    fileData = csv.reader(datafile, dialect='excel-tab')

    return fileData


def calcDist(ptA, pt3d):
    dx = ptA.getX() - pt3d.getX()
    dy = ptA.getY() - pt3d.getY()
    dz = ptA.getZ() - pt3d.getZ()

    dist = dx**2 + dy**2 + dz**2
    if dist != 0.0:
        dist = math.sqrt(dist)

    return dist


def flexibleLinkProps(dia, matl):
    connProps = apex.attribute.createConnectorProperties(
          stiffness               = 0.0,
          damping                 = 0.0,
          translationalStiffnessX = 0.0,
          translationalStiffnessY = 0.0,
          translationalStiffnessZ = 0.0,
          rotationalStiffnessX    = 0.0,
          rotationalStiffnessY    = 0.0,
          rotationalStiffnessZ    = 0.0,
          translationalDampingX   = 0.0,
          translationalDampingY   = 0.0,
          translationalDampingZ   = 0.0,
          rotationalDampingX      = 0.0,
          rotationalDampingY      = 0.0,
          rotationalDampingZ      = 0.0,
          diameter                = dia,
          linkMaterial            = matl)
    return connProps


def bushingProps(s1, s2, s3, s4, s5, s6):
    connProps = apex.attribute.createConnectorProperties(
          stiffness               = 0.0,
          damping                 = 0.0,
          translationalStiffnessX = s1,
          translationalStiffnessY = s2,
          translationalStiffnessZ = s3,
          rotationalStiffnessX    = s4,
          rotationalStiffnessY    = s5,
          rotationalStiffnessZ    = s6,
          translationalDampingX   = 0.0,
          translationalDampingY   = 0.0,
          translationalDampingZ   = 0.0,
          rotationalDampingX      = 0.0,
          rotationalDampingY      = 0.0,
          rotationalDampingZ      = 0.0,
          diameter                = 0.0,
          linkMaterial            = None)
    return connProps


def rigidLinkProps():
    connProps = apex.attribute.createConnectorProperties(
          stiffness               = 0.0,
          damping                 = 0.0,
          translationalStiffnessX = 0.0,
          translationalStiffnessY = 0.0,
          translationalStiffnessZ = 0.0,
          rotationalStiffnessX    = 0.0,
          rotationalStiffnessY    = 0.0,
          rotationalStiffnessZ    = 0.0,
          translationalDampingX   = 0.0,
          translationalDampingY   = 0.0,
          translationalDampingZ   = 0.0,
          rotationalDampingX      = 0.0,
          rotationalDampingY      = 0.0,
          rotationalDampingZ      = 0.0,
          diameter                = 0.0,
          linkMaterial            = None)
    return connProps


def getFastenerMaterial():
    matlName = "fastenerMatl"
    matlE    = 10.0E6
    matlNU   = 0.3
    matlRHO  = 0.000261598  #slinches

    allMatls = apex.catalog.getMaterials([{"path":apex.currentModel().getPath()}])
    matlExists = False
    for matl in allMatls:
        if matl.getName() == matlName:
            matlExists = True
            myMatl = matl
            break

    if not matlExists:
        myMatl = apex.catalog.createMaterial( name = matlName, description = "", color = [ 64, 254, 250 ] )
        myMatl.update(elasticModulus = matlE, poissonRatio = matlNU, density = matlRHO)

    return myMatl


def calcDistance(n1, n2):
    loc1 = n1.asNode().getLocation()
    loc2 = n2.asNode().getLocation()
    dx = loc2.getX() - loc1.getX()
    dy = loc2.getY() - loc1.getY()
    dz = loc2.getZ() - loc1.getZ()

    dist = dx**2 + dy**2 + dz**2

    if dist > 0.0: dist = math.sqrt(dist)

    return dist


def calcBushingStiffness(dia, length, matl):
    E  = matl.getElasticModulus()
    NU = matl.getPoissonRatio()
    G = E/2.0/(1.0+NU)

    if length <= 0.0: length = 1.0

    PI = 3.14159

    A = PI*dia**2/4.0
    I = PI*dia**4/64.0
    J = 2*I

    AS = 0.75*A

    s1 = E*A/length
    s2 = G*AS/length
    s3 = s2
    s4 = G*J/length
    s5 = E*I/length + G*AS*length/3.0
    s6 = s5

    stiff = [s1, s2, s3, s4, s5, s6]

    return stiff


def main(dict={}):
    myModel = apex.currentModel()

    fspec=dict["fspec"]
    tol          = float(dict["tolerance"])
    fastenerDia  = float(dict["diameter"])
    fastenerType = dict["fastenerType"]

    print("File:         "+fspec)
    print("Fastener Type:"+fastenerType)
    print("Diameter:     "+str(fastenerDia))
    print("Tolerance:    "+str(tol))

    unitSystem = r'''in-slinch-s-lbf-deg'''
    print("Using unit system: "+unitSystem)

    apex.setScriptUnitSystem(unitSystemName = unitSystem)

    if fastenerType == "":
        print("Fastener Type is BLANK!")
        return

    if fastenerType != "Rigid Link":
        fastenerMatl = getFastenerMaterial()
        print("Fastener Material: "+ fastenerMatl.getName())

    if tol <= 0.0:
        tol = 0.3
        print("Updated Tolerance: "+str(tol))

    fileData = readFile(fspec)

    parts = myModel.getParts(True)

    dia = fastenerDia

    connectors = []
    totalLines   = 0
    skippedLines = 0
    # parse the mwf file data into connectors
    # data[0] is the connector ID
    # data[1] is the number of Parts to be connected
    # data[2] is X
    # data[3] is Y
    # data[4] is Z
    # data[5] is a Part name
    # data[6] is a Part name
    # data[n] is a Part name
    # the number of parts should correspond to data[1]
    for data in fileData:
        totalLines += 1
        if data[0][0:1] == "#":
            skippedLines += 1
            continue
        nfields = len(data)
        connParts = []
        for d in data[5:nfields]:
            connParts.append(d.lstrip().rstrip())
        nParts = len(connParts)

        if nParts != int(data[1]):
            print("Mismatching Part count for connection "+str(data[0])+ " at line: "+str(totalLines))
            print("Parts expected: "+str(data[1]))
            print("Parts found: "+str(nParts))
            continue

        if nParts < 2:
            print("Less than 2 Parts defined for connection "+str(data[0])+ " at line: "+str(totalLines))
            continue

        cParts = []
        found = 0
        for connPart in connParts:
            for part in parts:
                if connPart == part.getName():
                    cParts.append(part)
                    found += 1
                    break
            if found == nParts: break

        if found != nParts:
            print("Did not find all Parts from the connector file!")
            for connPart in cParts:
                print("Connector Part: "+connPart.getName())
            continue

        # create point at the x, y, z location.  must create a point in each separate part that is being connected
        # reparent the points to the proper Part
        connData = []
        for connPart in cParts:
            myPath = part.getPath()[part.getPath().find("/")+1:]
            myModel.setCurrentPart(myPath+"/"+connPart.getName())
            connPoint = apex.geometry.createPointXYZ( x = float(data[2]), y = float(data[3]), z = float(data[4]))
            #_entities = apex.entityCollection()
            #_entities.append(connPoint)
            #apex.reparent(parent = connPart, target = _entities)

            connData.append([connPart, connPoint])

        connectors.append({"id":data[0], "data":connData})

    print("Total Lines: "+str(totalLines))
    print("Skipped Lines: "+str(skippedLines))

    # find the closest surface for each connector Point
    for conn in connectors:
        for data in conn["data"]:
            surfs = data[0].getSurfaces()  #data[0] is the Part
            maxDist = -1.0
            closeSurf = None
            for surf in surfs:
                x = data[1].getX()
                y = data[1].getY()
                z = data[1].getZ()
                pt = apex.Coordinate(x, y, z)
                pt3d = surf.evaluateClosestLocationOnSurface(pt)
                dist = calcDist(data[1], pt3d)
                if dist > maxDist:
                    maxDist = dist
                    closeSurf = surf
            data.append(maxDist+0.1)          # data[2]
            data.append(closeSurf.getName())  # data[3]
            data.append(closeSurf)            # data[4]

    # find all points to be associated to each surface.  it is much quicker to associate all points at once than one at a time
    # due to the generative re-mesh operation
    surfs    = []
    surfData = {}
    maxDist = -1.0
    for conn in connectors:
        for data in conn["data"]:
            s = data[0].getName()+"-"+data[3]  # data[0] is the part name, data[3] is the surface name
            if s in surfData.keys():
                surfData[s].append(data[1])
            else:
                surfData[s] = [data[1]]
                surfs.append([data[0], data[3], data[4]])  #data[0] is the part, data[3] is the surface name, data[4] is the surface object
            if data[2] > maxDist: maxDist = data[2]        # find the maximum maxDist

    # associate the points to the surfaces
    maxDist += 0.5
    for s in surfs:
        _sources = apex.entityCollection()
        partName = s[0].getName()
        surfName = s[1]
        for data in surfData[partName+"-"+surfName]:
            _sources += data.asPoint().getVertices()
        _targets = apex.entityCollection()
        _targets += s[2].getFaces()
        try:
            apex.mesh.createSeedPointsBySourceTarget(sources = _sources, targets = _targets, searchDistance = maxDist, cleanupTolerance = 0.01)
        except:
            print("Unable to associate point?")
            print("Part: "+partName)
            print("Surface: "+surfName)
            print("Point: "+data.asPoint().getName())

    if fastenerType == "Flexible Link":
        connectorProps = flexibleLinkProps(dia, fastenerMatl)
        _connectorType = apex.attribute.ConnectorType.FlexibleLink
    elif fastenerType == "Rigid Link":
        connectorProps = rigidLinkProps()
        _connectorType = apex.attribute.ConnectorType.RigidLink

    print("Creating fasteners: "+fastenerType)

    # THIS IS VERY INEFFICIENT.  I NEED TO IMPROVE THIS IN THE FUTURE
    numConnectorsCreated = 0
    for conn in connectors:
        endPoints = []
        for data in conn["data"]:
            nodes = data[4].getNodes()  # currently point-to-point fasteners must reference nodes.  as such, the surface must be meshed!  perhaps I should check this earlier
            if nodes.len() <= 0:
                print("Surface not meshed!  Surface: "+data[4].getName())
                return

            nn = apex.IPhysicalCollection()
            for node in nodes:
                nn.append(node)
            mySearch = apex.utility.ProximitySearch()
            mySearch.insertCollection(nn)
            obj = mySearch.findNearestObject(data[1].asPoint().getLocation())

            endPoints.append([apex.construct.createLocationByEntity(entity = obj.getNearestObject()), data[0]])

        _endPoint1 = endPoints[0][0]
        #n1 = obj.getNearestObject()
        for ep in endPoints[1:]:
            _endPoint2 = ep[0]

            if fastenerType == "Bushing":
                fastenerLength = calcDist(_endPoint1, _endPoint2)
                bushStiff = calcBushingStiffness(fastenerDia, fastenerLength, fastenerMatl)
                connectorProps = bushingProps(bushStiff[0], bushStiff[1], bushStiff[2], bushStiff[3], bushStiff[4], bushStiff[5])
                _connectorType = apex.attribute.ConnectorType.Bushing

            #_appMethod = apex.attribute.ApplicationMethod.Remote
            _appMethod = apex.attribute.ApplicationMethod.Direct
            c1 = connector_1 = apex.attribute.createConnector(
                name = endPoints[0][1].getName()+" - "+ep[1].getName(),
                connectorType = _connectorType,
                connectorProperties = connectorProps,
                applicationMethod1 = _appMethod,
                applicationMethod2 = _appMethod,
                end1InterfacePoint = _endPoint1,
                end2InterfacePoint = _endPoint2,
                description = "")

            c1.addUserAttribute(userAttributeName = "au_P2P", stringValue = "auFromMWF")

            c1.addUserAttribute(userAttributeName = "end1_X", floatValue = _endPoint1.getAssociatedEntity().asNode().getX())
            c1.addUserAttribute(userAttributeName = "end1_Y", floatValue = _endPoint1.getAssociatedEntity().asNode().getY())
            c1.addUserAttribute(userAttributeName = "end1_Z", floatValue = _endPoint1.getAssociatedEntity().asNode().getZ())

            c1.addUserAttribute(userAttributeName = "end2_X", floatValue = _endPoint2.getAssociatedEntity().asNode().getX())
            c1.addUserAttribute(userAttributeName = "end2_Y", floatValue = _endPoint2.getAssociatedEntity().asNode().getY())
            c1.addUserAttribute(userAttributeName = "end2_Z", floatValue = _endPoint2.getAssociatedEntity().asNode().getZ())

            numConnectorsCreated += 1
            _endPoint1 = _endPoint2


    print(str(numConnectorsCreated)+" connectors created")

    print("done")



