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

# ***************************************************************************
# *                                                                         *
# *   Copyright (c) 2024 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/>.                                      *
# *                                                                         *
# ***************************************************************************

import tempfile

import ifcopenshell

import FreeCAD
import Draft

from importers import exportIFC
from importers import exportIFCHelper
from importers import importIFCHelper

from . import ifc_tools
from . import ifc_import


PARAMS = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/NativeIFC")


def get_export_preferences(ifcfile, preferred_context=None, create=None):
    """returns a preferences dict for exportIFC.
    Preferred context can either indicate a ContextType like 'Model' or 'Plan',
    or a [ContextIdentifier,ContextType,TargetView] list or tuple, for ex.
    ('Annotation','Plan') or ('Body','Model','MODEL_VIEW'). This function
    will do its best to find the most appropriate context. If create is True,
    if the exact context is not found, a new one is created"""

    prefs = exportIFC.getPreferences()
    prefs["SCHEMA"] = ifcfile.wrapped_data.schema_name()
    s = ifcopenshell.util.unit.calculate_unit_scale(ifcfile)
    # the above lines yields meter -> file unit scale factor. We need mm
    prefs["SCALE_FACTOR"] = 0.001 / s
    cids = ifc_tools.get_body_context_ids(ifcfile)
    contexts = [ifcfile[i] for i in cids]
    best_context = None
    exact_match = False
    if preferred_context:
        if isinstance(preferred_context, str):
            for context in contexts:
                if context.ContextType == preferred_context:
                    best_context = context
                    exact_match = True
                    break
        elif isinstance(preferred_context, (list, tuple)):
            second_choice = None
            for context in contexts:
                if len(preferred_context) > 2:
                    if (
                        context.TargetView == preferred_context[2]
                        and context.ContextType == preferred_context[1]
                        and context.ContextIdentifier == preferred_context[0]
                    ):
                        best_context = context
                        exact_match = True
                if len(preferred_context) > 1:
                    if (
                        context.ContextType == preferred_context[1]
                        and context.ContextIdentifier == preferred_context[0]
                    ):
                        if not exact_match:
                            best_context = context
                            if len(preferred_context) == 2:
                                exact_match = True
                if context.ContextType == preferred_context[0]:
                    if not exact_match:
                        best_context = context
                        if len(preferred_context) == 1:
                            exact_match = True
            if contexts:
                if not best_context:
                    best_context = contexts[0]
        if create:
            if not exact_match:
                if isinstance(preferred_context, str):
                    best_context = ifc_tools.api_run(
                        "context.add_context", ifcfile, context_type=preferred_context
                    )
                elif best_context:
                    if len(preferred_context) > 2:
                        best_context = ifc_tools.api_run(
                            "context.add_context",
                            ifcfile,
                            context_type=preferred_context[1],
                            context_identifier=preferred_context[0],
                            target_view=preferred_context[2],
                            parent=best_context,
                        )
                    elif len(preferred_context) > 1:
                        best_context = ifc_tools.api_run(
                            "context.add_context",
                            ifcfile,
                            context_type=preferred_context[1],
                            context_identifier=preferred_context[0],
                            parent=best_context,
                        )
                else:
                    if len(preferred_context) > 1:
                        best_context = ifc_tools.api_run(
                            "context.add_context", ifcfile, context_type=preferred_context[1]
                        )
                        if len(preferred_context) > 2:
                            best_context = ifc_tools.api_run(
                                "context.add_context",
                                ifcfile,
                                context_type=preferred_context[1],
                                context_identifier=preferred_context[0],
                                target_view=preferred_context[2],
                                parent=best_context,
                            )
                        else:
                            best_context = ifc_tools.api_run(
                                "context.add_context",
                                ifcfile,
                                context_type=preferred_context[1],
                                context_identifier=preferred_context[0],
                                parent=best_context,
                            )
                    else:
                        best_context = ifc_tools.api_run(
                            "context.add_context", ifcfile, context_type=preferred_context[0]
                        )
    if not best_context:
        if contexts:
            best_context = contexts[0]
        else:
            best_context = ifc_tools.api_run("context.add_context", ifcfile, context_type="Model")
    return prefs, best_context


def create_product(obj, parent, ifcfile, ifcclass=None):
    """Creates an IFC product out of a FreeCAD object"""

    name = obj.Label
    description = getattr(obj, "Description", None)
    if not ifcclass:
        ifcclass = ifc_tools.get_ifctype(obj)
    representation, placement = create_representation(obj, ifcfile)
    product = ifc_tools.api_run("root.create_entity", ifcfile, ifc_class=ifcclass, name=name)
    ifc_tools.set_attribute(ifcfile, product, "Description", description)
    ifc_tools.set_attribute(ifcfile, product, "ObjectPlacement", placement)
    # TODO below cannot be used at the moment because the ArchIFC exporter returns an
    # IfcProductDefinitionShape already and not an IfcShapeRepresentation
    # ifc_tools.api_run("geometry.assign_representation", ifcfile, product=product, representation=representation)
    ifc_tools.set_attribute(ifcfile, product, "Representation", representation)
    # TODO treat subtractions/additions
    return product


def create_representation(obj, ifcfile):
    """Creates a geometry representation for the given object"""

    # TEMPORARY use the Arch exporter
    # TODO this is temporary. We should rely on ifcopenshell for this with:
    # https://blenderbim.org/docs-python/autoapi/ifcopenshell/api/root/create_entity/index.html
    # a new FreeCAD 'engine' should be added to:
    # https://blenderbim.org/docs-python/autoapi/ifcopenshell/api/geometry/index.html
    # that should contain all typical use cases one could have to convert FreeCAD geometry
    # to IFC.

    # setup exporter - TODO do that in the module init
    exportIFC.clones = {}
    exportIFC.profiledefs = {}
    exportIFC.surfstyles = {}
    exportIFC.shapedefs = {}
    exportIFC.ifcopenshell = ifcopenshell
    exportIFC.ifcbin = exportIFCHelper.recycler(ifcfile, template=False)
    prefs, context = get_export_preferences(ifcfile)
    representation, placement, shapetype = exportIFC.getRepresentation(
        ifcfile, context, obj, preferences=prefs
    )
    return representation, placement


def get_object_type(ifcentity, objecttype=None):
    """Determines a creation type for this object"""

    if not objecttype:
        if ifcentity.is_a("IfcAnnotation"):
            if get_sectionplane(ifcentity):
                objecttype = "sectionplane"
            elif get_dimension(ifcentity):
                objecttype = "dimension"
            elif get_text(ifcentity):
                objecttype = "text"
        elif ifcentity.is_a("IfcGridAxis"):
            objecttype = "axis"
        elif ifcentity.is_a("IfcControl"):
            objecttype = "schedule"
        elif ifcentity.is_a() in ["IfcBuilding", "IfcBuildingStorey"]:
            objecttype = "buildingpart"
    return objecttype


def is_annotation(obj):
    """Determines if the given FreeCAD object should be saved as an IfcAnnotation"""

    if getattr(obj, "IfcClass", None) in ["IfcAnnotation", "IfcGridAxis"]:
        return True
    if getattr(obj, "IfcType", None) == "Annotation":
        return True
    if obj.isDerivedFrom("Part::Part2DObject"):
        return True
    elif obj.isDerivedFrom("App::Annotation"):
        return True
    elif Draft.getType(obj) in [
        "BezCurve",
        "BSpline",
        "Wire",
        "DraftText",
        "Text",
        "Dimension",
        "LinearDimension",
        "AngularDimension",
        "SectionPlane",
    ]:
        return True
    elif obj.isDerivedFrom("Part::Feature"):
        if obj.Shape and (not obj.Shape.Solids) and obj.Shape.Edges:
            if not obj.Shape.Faces:
                return True
            elif (
                (obj.Shape.BoundBox.XLength < 0.0001)
                or (obj.Shape.BoundBox.YLength < 0.0001)
                or (obj.Shape.BoundBox.ZLength < 0.0001)
            ):
                return True
    return False


def get_text(annotation):
    """Determines if an IfcAnnotation contains an IfcTextLiteral.
    Returns the IfcTextLiteral or None"""

    if annotation.is_a("IfcAnnotation"):
        for rep in annotation.Representation.Representations:
            for item in rep.Items:
                if item.is_a("IfcTextLiteral"):
                    return item
    return None


def get_dimension(annotation):
    """Determines if an IfcAnnotation is representing a dimension.
    Returns a list containing the representation, two points indicating
    the measured points, and optionally a third point indicating where
    the dimension line is located, if available"""

    if annotation.is_a("IfcAnnotation"):
        if annotation.ObjectType == "DIMENSION":
            s = ifcopenshell.util.unit.calculate_unit_scale(annotation.file) * 1000
            for rep in annotation.Representation.Representations:
                shape = importIFCHelper.get2DShape(rep, s, notext=True)
                pl = get_placement(annotation.ObjectPlacement, scale=s)
                if pl:
                    shape[0].Placement = pl
                if shape and len(shape) == 1:
                    if len(shape[0].Vertexes) >= 2:
                        # two-point polyline (BBIM)
                        res = [rep, shape[0].Vertexes[0].Point, shape[0].Vertexes[-1].Point]
                        if len(shape[0].Vertexes) > 2:
                            # 4-point polyline (FreeCAD)
                            res.append(shape[0].Vertexes[1].Point)
                        return res
                else:
                    print(annotation, "NOT A DIMENSION")
    return None


def get_sectionplane(annotation):
    """Determines if an IfcAnnotation is representing a section plane.
    Returns a list containing a placement, and optionally an X dimension,
    an Y dimension and a  depth dimension"""

    if annotation.is_a("IfcAnnotation"):
        if annotation.ObjectType == "DRAWING":
            s = ifcopenshell.util.unit.calculate_unit_scale(annotation.file) * 1000
            result = [get_placement(annotation.ObjectPlacement, scale=s)]
            for rep in annotation.Representation.Representations:
                for item in rep.Items:
                    if item.is_a("IfcCsgSolid"):
                        if item.TreeRootExpression.is_a("IfcBlock"):
                            result.append(item.TreeRootExpression.XLength * s)
                            result.append(item.TreeRootExpression.YLength * s)
                            result.append(item.TreeRootExpression.ZLength * s)
            return result
    return None


def get_axis(obj):
    """Determines if a given IFC entity is an IfcGridAxis. Returns a tuple
    containing a Placement, a length value in millimeters, and a tag"""

    if obj.is_a("IfcGridAxis"):
        tag = obj.AxisTag
        s = ifcopenshell.util.unit.calculate_unit_scale(obj.file) * 1000
        shape = importIFCHelper.get2DShape(obj.AxisCurve, s, notext=True)
        if shape:
            edge = shape[0].Edges[0]  # we suppose here the axis shape is a single straight line
            if obj.SameSense:
                p0 = edge.Vertexes[0].Point
                p1 = edge.Vertexes[-1].Point
            else:
                p0 = edge.Vertexes[-1].Point
                p1 = edge.Vertexes[0].Point
            length = edge.Length
            placement = FreeCAD.Placement()
            placement.Base = p0
            placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 1, 0), p1.sub(p0))
            return (placement, length, tag)
    return None


def create_annotation(obj, ifcfile):
    """Adds an IfcAnnotation from the given object to the given IFC file"""

    exportIFC.clones = {}
    exportIFC.profiledefs = {}
    exportIFC.surfstyles = {}
    exportIFC.shapedefs = {}
    exportIFC.curvestyles = {}
    exportIFC.ifcopenshell = ifcopenshell
    exportIFC.ifcbin = exportIFCHelper.recycler(ifcfile, template=False)
    if is_annotation(obj) and Draft.getType(obj) != "SectionPlane":
        context_type = "Plan"
    else:
        context_type = "Model"
    prefs, context = get_export_preferences(ifcfile, preferred_context=context_type, create=True)
    prefs["BBIMDIMS"] = True  # Save dimensions as 2-point polylines
    history = get_history(ifcfile)
    # TODO The following prints each edge as a separate IfcGeometricCurveSet
    # It should be refined to create polylines instead
    anno = exportIFC.create_annotation(obj, ifcfile, context, history, prefs)
    return anno


def get_history(ifcfile):
    """Returns the owner history or None"""

    history = ifcfile.by_type("IfcOwnerHistory")
    if history:
        history = history[0]
    else:
        # IFC4 allows to not write any history
        history = None
    return history


def get_placement(ifcelement, ifcfile=None, scale=None):
    """Returns a FreeCAD placement from an IFC placement"""

    if not scale:
        if not ifcfile:
            ifcfile = ifcelement.file
        scale = 0.001 / ifcopenshell.util.unit.calculate_unit_scale(ifcfile)
    return importIFCHelper.getPlacement(ifcelement, scaling=scale)


def get_scaled_point(point, ifcfile=None, is2d=False):
    """Returns a scaled 2d or 3d point tuple form a FreeCAD point"""

    if not ifcfile:
        ifcfile = ifcelement.file
    s = 0.001 / ifcopenshell.util.unit.calculate_unit_scale(ifcfile)
    v = FreeCAD.Vector(point)
    v.multiply(s)
    v = tuple(v)
    if is2d:
        v = v[:2]
    return v


def get_scaled_value(value, ifcfile):
    """Returns a scaled dimension value"""

    s = 0.001 / ifcopenshell.util.unit.calculate_unit_scale(ifcfile)
    return value * s


def export_and_convert(objs, doc):
    """Exports the given objects and their descendents to the given IFC file
    and re-imports it into the given document. This is slower than direct_conversion()
    but gives an intermediate file which can be useful for debugging"""

    tf = tempfile.mkstemp(suffix=".ifc")[1]
    exportIFC.export(objs, tf)
    ifc_import.insert(tf, doc.Name, singledoc=True)


def direct_conversion(objs, doc):
    """Exports the given objects to the given ifcfile and recreates the contents"""

    prj_obj = ifc_tools.convert_document(doc, silent=True)
    exportIFC.export(objs, doc.Proxy.ifcfile)
    if PARAMS.GetBool("LoadOrphans", True):
        ifc_tools.load_orphans(prj_obj)
    if PARAMS.GetBool("LoadMaterials", False):
        ifc_materials.load_materials(prj_obj)
    if PARAMS.GetBool("LoadLayers", False):
        ifc_layers.load_layers(prj_obj)
    if PARAMS.GetBool("LoadPsets", False):
        ifc_psets.load_psets(prj_obj)
