# SPDX-License-Identifier: LGPL-2.1-or-later

# ***************************************************************************
# *                                                                         *
# *   Copyright (c) 2020 Yorik van Havre <yorik@uncreated.net>              *
# *                                                                         *
# *   This file is part of FreeCAD.                                         *
# *                                                                         *
# *   FreeCAD is free software: you can redistribute it and/or modify it    *
# *   under the terms of the GNU Lesser General Public License as           *
# *   published by the Free Software Foundation, either version 2.1 of the  *
# *   License, or (at your option) any later version.                       *
# *                                                                         *
# *   FreeCAD is distributed in the hope that it will be useful, but        *
# *   WITHOUT ANY WARRANTY; without even the implied warranty of            *
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      *
# *   Lesser General Public License for more details.                       *
# *                                                                         *
# *   You should have received a copy of the GNU Lesser General Public      *
# *   License along with FreeCAD. If not, see                               *
# *   <https://www.gnu.org/licenses/>.                                      *
# *                                                                         *
# ***************************************************************************


__title__ = "FreeCAD structural IFC export tools"
__author__ = "Yorik van Havre"
__url__ = "https://www.freecad.org"

ALLOW_LINEAR_OBJECTS = True  # allow non-solid objects (wires, etc) to become analytic objects?

structural_nodes = {}  # this keeps track of nodes during this session
structural_curves = {}  # this keeps track of structural curves during this session
scaling = 1.0  # this keeps track of scaling during this session


def setup(ifcfile, ifcbin, scale):
    """Creates all the needed setup for structural model."""

    global structural_nodes, scaling
    structural_nodes = {}
    scaling = scale
    import ifcopenshell

    uid = ifcopenshell.guid.new
    ownerHistory = ifcfile.by_type("IfcOwnerHistory")[0]
    project = ifcfile.by_type("IfcProject")[0]
    structContext = createStructuralContext(ifcfile)
    if ifcfile.wrapped_data.schema_name() == "IFC2X3":
        mod = ifcfile.createIfcStructuralAnalysisModel(
            uid(),
            ownerHistory,
            "Structural Analysis Model",
            None,
            None,
            "NOTDEFINED",
            None,
            None,
            None,
        )
    else:
        localPlacement = ifcbin.createIfcLocalPlacement()
        structModel = ifcfile.createIfcStructuralAnalysisModel(
            uid(),
            ownerHistory,
            "Structural Analysis Model",
            None,
            None,
            "NOTDEFINED",
            None,
            None,
            None,
            localPlacement,
        )
        relation = ifcfile.createIfcRelDeclares(
            uid(), ownerHistory, None, None, project, [structModel]
        )


def createStructuralContext(ifcfile):
    """Creates an additional geometry context for structural objects. Returns the new context"""

    contexts = ifcfile.by_type("IfcGeometricRepresentationContext")
    # filter out subcontexts
    contexts = [c for c in contexts if c.is_a() == "IfcGeometricRepresentationContext"]
    geomContext = contexts[0]  # arbitrarily take the first one...
    structContext = ifcfile.createIfcGeometricRepresentationSubContext(
        "Analysis", "Axis", None, None, None, None, geomContext, None, "GRAPH_VIEW", None
    )
    return structContext


def getStructuralContext(ifcfile):
    """Returns the structural context from the file"""
    for c in ifcfile.by_type("IfcGeometricRepresentationSubContext"):
        if c.ContextIdentifier == "Analysis":
            return c


def createStructuralNode(ifcfile, ifcbin, point):
    """Creates a connection node at the given point"""

    import ifcopenshell

    uid = ifcopenshell.guid.new
    ownerHistory = ifcfile.by_type("IfcOwnerHistory")[0]
    structContext = getStructuralContext(ifcfile)
    cartPoint = ifcbin.createIfcCartesianPoint(tuple(point))
    vertPoint = ifcfile.createIfcVertexPoint(cartPoint)
    topologyRep = ifcfile.createIfcTopologyRepresentation(
        structContext, "Analysis", "Vertex", [vertPoint]
    )
    prodDefShape = ifcfile.createIfcProductDefinitionShape(None, None, [topologyRep])
    # boundary conditions serve for ex. to create fixed nodes
    # appliedCondition = ifcfile.createIfcBoundaryNodeCondition(
    #     "Fixed",ifcfile.createIfcBoolean(True), ifcfile.createIfcBoolean(True), ifcfile.createIfcBoolean(True),
    #     ifcfile.createIfcBoolean(True), ifcfile.createIfcBoolean(True), ifcfile.createIfcBoolean(True))
    # for now we don't create any boundary condition
    appliedCondition = None
    localPlacement = ifcbin.createIfcLocalPlacement()
    if ifcfile.wrapped_data.schema_name() == "IFC2X3":
        structPntConn = ifcfile.createIfcStructuralPointConnection(
            uid(),
            ownerHistory,
            "Vertex",
            None,
            None,
            localPlacement,
            prodDefShape,
            appliedCondition,
        )
    else:
        structPntConn = ifcfile.createIfcStructuralPointConnection(
            uid(),
            ownerHistory,
            "Vertex",
            None,
            None,
            localPlacement,
            prodDefShape,
            appliedCondition,
            None,
        )
    return structPntConn


def createStructuralCurve(ifcfile, ifcbin, curve):
    """Creates a structural connection for a curve"""

    import ifcopenshell

    uid = ifcopenshell.guid.new
    ownerHistory = ifcfile.by_type("IfcOwnerHistory")[0]
    structContext = getStructuralContext(ifcfile)

    cartPnt1 = ifcbin.createIfcCartesianPoint(tuple(curve.Vertexes[0].Point.multiply(scaling)))
    cartPnt2 = ifcbin.createIfcCartesianPoint(tuple(curve.Vertexes[-1].Point.multiply(scaling)))
    vertPnt1 = ifcfile.createIfcVertexPoint(cartPnt1)
    vertPnt2 = ifcfile.createIfcVertexPoint(cartPnt2)
    edge = ifcfile.createIfcEdge(vertPnt1, vertPnt2)
    topologyRep = ifcfile.createIfcTopologyRepresentation(structContext, "Analysis", "Edge", [edge])
    prodDefShape = ifcfile.createIfcProductDefinitionShape(None, None, [topologyRep])

    # boundary conditions serve for ex. to create fixed edges
    # for now we don't create any boundary condition
    appliedCondition = None
    localPlacement = ifcbin.createIfcLocalPlacement()
    origin = ifcfile.createIfcCartesianPoint((0.0, 0.0, 0.0))
    orientation = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
    xAxis = ifcfile.createIfcDirection(tuple(orientation[0]))
    zAxis = ifcfile.createIfcDirection(tuple(orientation[2]))
    localAxes = ifcfile.createIfcAxis2Placement3D(origin, zAxis, xAxis)
    structCrvConn = ifcfile.createIfcStructuralCurveConnection(
        uid(),
        ownerHistory,
        "Line",
        None,
        None,
        localPlacement,
        prodDefShape,
        appliedCondition,
        localAxes,
    )
    return structCrvConn


def createStructuralMember(ifcfile, ifcbin, obj):
    """Creates a structural member if possible. Returns the member"""

    global structural_nodes, structural_curves
    structuralMember = None
    import Draft
    import Part
    import ifcopenshell
    import FreeCAD

    uid = ifcopenshell.guid.new
    ownerHistory = ifcfile.by_type("IfcOwnerHistory")[0]
    structContext = getStructuralContext(ifcfile)

    # find edges to convert into structural members
    edges = None
    if Draft.getType(obj) not in ["Structure"]:
        # for non structural elements
        if ALLOW_LINEAR_OBJECTS and obj.isDerivedFrom("Part::Feature"):
            # for objects created with Part workbench
            if obj.Shape.Faces:
                # for objects with faces
                return None
            elif not obj.Shape.Edges:
                # for objects without edges
                return None
            else:
                edges = obj.Shape.Edges
    else:
        # for structural elements with nodes
        nodes = [obj.Placement.multVec(n) for n in obj.Nodes]
        if len(nodes) > 2:
            # when there are more then 2 nodes (i.e. for slabs) append closing node to produce closing edge
            nodes.append(nodes[0])
        wire = Part.makePolygon(nodes)
        edges = wire.Edges
    if not edges:
        return None

    # OBJECT CLASSIFICATION by edge number
    # Linear elements for edge_number = 1, Surface elements for edge_number > 1
    # we don't care about curved edges just now...

    if len(edges) == 1:
        # LINEAR OBJECTS: beams, columns
        # ATM limitations:
        # - no profile properties are taken into account
        # - no materials properties are takein into account
        # -
        # create geometry
        verts = [None for _ in range(len(edges) + 1)]
        verts[0] = tuple(edges[0].Vertexes[0].Point.multiply(scaling))
        verts[1] = tuple(edges[0].Vertexes[-1].Point.multiply(scaling))
        cartPnt1 = ifcfile.createIfcCartesianPoint(verts[0])
        cartPnt2 = ifcfile.createIfcCartesianPoint(verts[1])
        vertPnt1 = ifcfile.createIfcVertexPoint(cartPnt1)
        vertPnt2 = ifcfile.createIfcVertexPoint(cartPnt2)
        newEdge = ifcfile.createIfcEdge(vertPnt1, vertPnt2)
        topologyRep = ifcfile.createIfcTopologyRepresentation(
            structContext, "Analysis", "Edge", (newEdge,)
        )
        prodDefShape = ifcfile.createIfcProductDefinitionShape(None, None, (topologyRep,))
        # set local coordinate system
        localPlacement = ifcbin.createIfcLocalPlacement()
        localZAxis = ifcbin.createIfcDirection((0, 0, 1))
        # create structural member
        if ifcfile.wrapped_data.schema_name() == "IFC2X3":
            structuralMember = ifcfile.createIfcStructuralCurveMember(
                uid(),
                ownerHistory,
                obj.Label,
                None,
                None,
                localPlacement,
                prodDefShape,
                "RIGID_JOINED_MEMBER",
            )
        else:
            localZAxis = ifcbin.createIfcDirection((0, 0, 1))
            structuralMember = ifcfile.createIfcStructuralCurveMember(
                uid(),
                ownerHistory,
                obj.Label,
                None,
                None,
                localPlacement,
                prodDefShape,
                "RIGID_JOINED_MEMBER",
                localZAxis,
            )

    elif len(edges) > 1:
        # SURFACE OBJECTS: slabs (horizontal, vertical, inclined)
        # ATM limitations:
        # - mo material properties are taken into account
        # - walls don't work because they miss a node system
        # -
        # creates geometry
        verts = [None for _ in range(len(edges))]
        for i, edge in enumerate(edges):
            verts[i] = tuple(edge.Vertexes[0].Point.multiply(scaling))
            cartPnt = ifcfile.createIfcCartesianPoint(verts[i])
            vertPnt = ifcfile.createIfcVertexPoint(cartPnt)
        orientedEdges = [None for _ in range(len(edges))]
        for i, vert in enumerate(verts):
            v2Index = (i + 1) if i < len(verts) - 1 else 0
            cartPnt1 = ifcfile.createIfcCartesianPoint(vert)
            cartPnt2 = ifcfile.createIfcCartesianPoint(verts[v2Index])
            vertPnt1 = ifcfile.createIfcVertexPoint(cartPnt1)
            vertPnt2 = ifcfile.createIfcVertexPoint(cartPnt2)
            edge = ifcfile.createIfcEdge(vertPnt1, vertPnt2)
            orientedEdges[i] = ifcfile.createIfcOrientedEdge(None, None, edge, True)
        edgeLoop = ifcfile.createIfcEdgeLoop(tuple(orientedEdges))
        # sets local coordinate system
        localPlacement = ifcbin.createIfcLocalPlacement()
        # sets face origin to the first vertex point of the planar surface
        origin = cartPnt2
        # find crossVect that is perpendicular to the planar surface
        vect0 = FreeCAD.Vector(verts[0])
        vect1 = FreeCAD.Vector(verts[1])
        vectn = FreeCAD.Vector(verts[-1])
        vect01 = vect1.sub(vect0)
        vect0n = vectn.sub(vect0)
        crossVect = vect01.cross(vect0n)
        # normalize crossVect
        normVect = crossVect.normalize()
        xAxis = ifcfile.createIfcDirection(tuple([vect01.x, vect01.y, vect01.z]))
        zAxis = ifcfile.createIfcDirection(tuple([normVect.x, normVect.y, normVect.z]))
        localAxes = ifcfile.createIfcAxis2Placement3D(origin, zAxis, xAxis)
        plane = ifcfile.createIfcPlane(localAxes)
        faceBound = ifcfile.createIfcFaceBound(edgeLoop, True)
        face = ifcfile.createIfcFaceSurface((faceBound,), plane, True)
        topologyRep = ifcfile.createIfcTopologyRepresentation(
            structContext, "Analysis", "Face", (face,)
        )
        prodDefShape = ifcfile.createIfcProductDefinitionShape(None, None, (topologyRep,))
        # sets surface thickness
        # TODO: ATM limitations
        # - for vertical slabs (walls) or inclined slabs (ramps) the thickness is taken from the Height property
        thickness = float(obj.Height) * scaling
        # creates structural member
        structuralMember = ifcfile.createIfcStructuralSurfaceMember(
            uid(),
            ownerHistory,
            obj.Label,
            None,
            None,
            localPlacement,
            prodDefShape,
            "SHELL",
            thickness,
        )

    # check for existing connection nodes
    for vert in verts:
        vertCoord = tuple(vert)
        if vertCoord in structural_nodes:
            if structural_nodes[vertCoord]:
                # there is already another member using this point
                structPntConn = structural_nodes[vertCoord]
            else:
                # there is another member with same point, create a new node connection
                structPntConn = createStructuralNode(ifcfile, ifcbin, vert)
                structural_nodes[vertCoord] = structPntConn
            ifcfile.createIfcRelConnectsStructuralMember(
                uid(),
                ownerHistory,
                None,
                None,
                structuralMember,
                structPntConn,
                None,
                None,
                None,
                None,
            )
        else:
            # just add the point, no other member using it yet
            structural_nodes[vertCoord] = None

    # check for existing connection curves
    for edge in edges:
        verts12 = tuple(
            [
                edge.Vertexes[0].Point.x,
                edge.Vertexes[0].Point.y,
                edge.Vertexes[0].Point.z,
                edge.Vertexes[-1].Point.x,
                edge.Vertexes[-1].Point.y,
                edge.Vertexes[-1].Point.z,
            ]
        )
        verts21 = tuple(
            [
                edge.Vertexes[-1].Point.x,
                edge.Vertexes[-1].Point.y,
                edge.Vertexes[-1].Point.z,
                edge.Vertexes[0].Point.x,
                edge.Vertexes[0].Point.y,
                edge.Vertexes[0].Point.z,
            ]
        )
        verts12_in_curves = verts12 in structural_curves
        verts21_in_curves = verts21 in structural_curves
        if verts21_in_curves:
            verts = verts21
        else:
            verts = verts12
        if verts12_in_curves or verts21_in_curves:
            if structural_curves[verts]:
                # there is already another member using this curve
                strucCrvConn = structural_curves[verts]
            else:
                # there is another member with same edge, create a new curve connection
                strucCrvConn = createStructuralCurve(ifcfile, ifcbin, edge)
                structural_curves[verts] = strucCrvConn
            ifcfile.createIfcRelConnectsStructuralMember(
                uid(), None, None, None, structuralMember, strucCrvConn, None, None, None, None
            )
        else:
            # just add the curve, no other member using it yet
            structural_curves[verts] = None
    return structuralMember


def createStructuralGroup(ifcfile):
    """Assigns all structural objects found in the file to the structural model"""

    import ifcopenshell

    uid = ifcopenshell.guid.new
    ownerHistory = ifcfile.by_type("IfcOwnerHistory")[0]
    structSrfMember = ifcfile.by_type("IfcStructuralSurfaceMember")
    structCrvMember = ifcfile.by_type("IfcStructuralCurveMember")
    structPntConn = ifcfile.by_type("IfcStructuralPointConnection")
    structCrvConn = ifcfile.by_type("IfcStructuralCurveConnection")
    structModel = ifcfile.by_type("IfcStructuralAnalysisModel")[0]
    if structModel:
        members = structSrfMember + structCrvMember + structPntConn + structCrvConn
        if members:
            ifcfile.createIfcRelAssignsToGroup(
                uid(), ownerHistory, None, None, members, "PRODUCT", structModel
            )


def associates(ifcfile, aobj, sobj):
    """Associates an arch object with a struct object"""

    # This is probably not the right way to do this, ie. relate a structural
    # object with an IfcProduct. Needs to investigate more....

    import ifcopenshell

    uid = ifcopenshell.guid.new
    ownerHistory = ifcfile.by_type("IfcOwnerHistory")[0]
    ifcfile.createIfcRelAssignsToProduct(uid(), ownerHistory, None, None, [sobj], None, aobj)
