# ***************************************************************************
# *                                                                         *
# *   Copyright (c) 2023 Yorik van Havre <yorik@uncreated.net>              *
# *                                                                         *
# *   This program is free software; you can redistribute it and/or modify  *
# *   it under the terms of the GNU General Public License (GPL)            *
# *   as published by the Free Software Foundation; either version 3 of     *
# *   the License, or (at your option) any later version.                   *
# *   for detail see the LICENCE text file.                                 *
# *                                                                         *
# *   This program 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 General Public License for more details.                          *
# *                                                                         *
# *   You should have received a copy of the GNU Library General Public     *
# *   License along with this program; if not, write to the Free Software   *
# *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
# *   USA                                                                   *
# *                                                                         *
# ***************************************************************************

"""Unit test for the Native IFC module"""

import os
import time
import tempfile
import FreeCAD
import Draft
import Arch
import unittest
import requests
from nativeifc import ifc_import
from nativeifc import ifc_tools
from nativeifc import ifc_geometry
from nativeifc import ifc_materials
from nativeifc import ifc_layers
from nativeifc import ifc_psets
from nativeifc import ifc_objects
from nativeifc import ifc_generator
import ifcopenshell
from ifcopenshell.util import element
import difflib

IFCOPENHOUSE_IFC4 = (
    "https://github.com/aothms/IfcOpenHouse/raw/master/IfcOpenHouse_IFC4.ifc"
)
IFC_FILE_PATH = None  # downloaded IFC file path
FCSTD_FILE_PATH = None  # saved FreeCAD file
PARAMS = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/NativeIFC")
SINGLEDOC = False  # This allows to force singledoc mode for all tests
SDU = int(SINGLEDOC)  # number of objects is different in singledoc

"""
unit tests for the NativeIFC functionality. To run the tests, either:
- in terminal mode: FreeCAD -t ifc_selftest
- in the FreeCAD UI: Switch to Test Framework workbench, press "Self test" and
  choose ifc_selftest in the list
"""


def getIfcFilePath():
    global IFC_FILE_PATH
    if not IFC_FILE_PATH:
        path = tempfile.mkstemp(suffix=".ifc")[1]
        results = requests.get(IFCOPENHOUSE_IFC4)
        with open(path, "wb") as f:
            f.write(results.content)
        IFC_FILE_PATH = path
    return IFC_FILE_PATH


def clearObjects():
    names = [o.Name for o in FreeCAD.getDocument("IfcTest").Objects]
    for n in names:
        FreeCAD.getDocument("IfcTest").removeObject(n)


def compare(file1, file2):
    with open(file1) as f1:
        f1_text = f1.readlines()
    with open(file2) as f2:
        f2_text = f2.readlines()
    res = [
        l
        for l in difflib.unified_diff(
            f1_text, f2_text, fromfile=file1, tofile=file2, lineterm=""
        )
    ]
    res = [l for l in res if l.startswith("+") or l.startswith("-")]
    res = [l for l in res if not l.startswith("+++") and not l.startswith("---")]
    return res


class NativeIFCTest(unittest.TestCase):
    def setUp(self):
        # setting a new document to hold the tests
        if FreeCAD.ActiveDocument:
            if FreeCAD.ActiveDocument.Name != "IfcTest":
                FreeCAD.newDocument("IfcTest")
        else:
            FreeCAD.newDocument("IfcTest")
        FreeCAD.setActiveDocument("IfcTest")

    def tearDown(self):
        FreeCAD.closeDocument("IfcTest")
        pass

    def test01_ImportCoinSingle(self):
        FreeCAD.Console.PrintMessage(
            "1.  NativeIFC import: Single object, coin mode..."
        )
        clearObjects()
        fp = getIfcFilePath()
        ifc_import.insert(
            fp,
            "IfcTest",
            strategy=0,
            shapemode=1,
            switchwb=0,
            silent=True,
            singledoc=SINGLEDOC,
        )
        fco = len(FreeCAD.getDocument("IfcTest").Objects)
        self.assertTrue(fco == 1 - SDU, "ImportCoinSingle failed")

    def test02_ImportCoinStructure(self):
        FreeCAD.Console.PrintMessage(
            "2.  NativeIFC import: Model structure, coin mode..."
        )
        clearObjects()
        fp = getIfcFilePath()
        ifc_import.insert(
            fp,
            "IfcTest",
            strategy=1,
            shapemode=1,
            switchwb=0,
            silent=True,
            singledoc=SINGLEDOC,
        )
        fco = len(FreeCAD.getDocument("IfcTest").Objects)
        self.assertTrue(fco == 4 - SDU, "ImportCoinStructure failed")

    def test03_ImportCoinFull(self):
        global FCSTD_FILE_PATH
        FreeCAD.Console.PrintMessage("3.  NativeIFC import: Full model, coin mode...")
        clearObjects()
        fp = getIfcFilePath()
        d = ifc_import.insert(
            fp,
            "IfcTest",
            strategy=2,
            shapemode=1,
            switchwb=0,
            silent=True,
            singledoc=SINGLEDOC,
        )
        path = tempfile.mkstemp(suffix=".FCStd")[1]
        d.saveAs(path)
        FCSTD_FILE_PATH = path
        fco = len(FreeCAD.getDocument("IfcTest").Objects)
        self.assertTrue(fco > 4 - SDU, "ImportCoinFull failed")

    def test04_ImportShapeFull(self):
        FreeCAD.Console.PrintMessage("4.  NativeIFC import: Full model, shape mode...")
        clearObjects()
        fp = getIfcFilePath()
        d = ifc_import.insert(
            fp,
            "IfcTest",
            strategy=2,
            shapemode=0,
            switchwb=0,
            silent=True,
            singledoc=SINGLEDOC,
        )
        fco = len(FreeCAD.getDocument("IfcTest").Objects)
        self.assertTrue(fco > 4 - SDU, "ImportShapeFull failed")

    def test05_ImportFreeCAD(self):
        FreeCAD.Console.PrintMessage(
            "5.  NativeIFC FreeCAD import: NativeIFC coin file..."
        )
        clearObjects()
        doc = FreeCAD.open(FCSTD_FILE_PATH)
        obj = doc.Objects[-1]
        proj = ifc_tools.get_project(obj)
        ifcfile = ifc_tools.get_ifcfile(proj)
        print(ifcfile)
        self.assertTrue(ifcfile, "ImportFreeCAD failed")

    def test06_ModifyObjects(self):
        FreeCAD.Console.PrintMessage("6.  NativeIFC Modifying IFC document...")
        doc = FreeCAD.open(FCSTD_FILE_PATH)
        obj = doc.Objects[-1]
        obj.Label = "Modified name"
        proj = ifc_tools.get_project(obj)
        proj.IfcFilePath = proj.IfcFilePath[:-4] + "_modified.ifc"
        ifc_tools.save_ifc(proj)
        ifc_diff = compare(IFC_FILE_PATH, proj.IfcFilePath)
        obj.ShapeMode = 0
        obj.Proxy.execute(obj)
        self.assertTrue(
            obj.Shape.Volume > 2 and len(ifc_diff) <= 5, "ModifyObjects failed"
        )

    def test07_CreateDocument(self):
        FreeCAD.Console.PrintMessage("7.  NativeIFC Creating new IFC document...")
        doc = FreeCAD.ActiveDocument
        ifc_tools.create_document(doc, silent=True)
        fco = len(FreeCAD.getDocument("IfcTest").Objects)
        print(FreeCAD.getDocument("IfcTest").Objects)
        self.assertTrue(fco == 1 - SDU, "CreateDocument failed")

    def test08_ChangeIFCSchema(self):
        FreeCAD.Console.PrintMessage("8.  NativeIFC Changing IFC schema...")
        clearObjects()
        fp = getIfcFilePath()
        ifc_import.insert(
            fp,
            "IfcTest",
            strategy=2,
            shapemode=1,
            switchwb=0,
            silent=True,
            singledoc=SINGLEDOC,
        )
        obj = FreeCAD.getDocument("IfcTest").Objects[-1]
        proj = ifc_tools.get_project(obj)
        oldid = obj.StepId
        proj.Proxy.silent = True
        proj.Schema = "IFC2X3"
        FreeCAD.getDocument("IfcTest").recompute()
        self.assertTrue(obj.StepId != oldid, "ChangeIFCSchema failed")

    def test09_CreateBIMObjects(self):
        FreeCAD.Console.PrintMessage("9.  NativeIFC Creating BIM objects...")
        doc = FreeCAD.ActiveDocument
        proj = ifc_tools.create_document(doc, silent=True)
        site = Arch.makeSite()
        site = ifc_tools.aggregate(site, proj)
        bldg = Arch.makeBuilding()
        bldg = ifc_tools.aggregate(bldg, site)
        storey = Arch.makeFloor()
        storey = ifc_tools.aggregate(storey, bldg)
        wall = Arch.makeWall(None, 200, 400, 20)
        wall = ifc_tools.aggregate(wall, storey)
        column = Arch.makeStructure(None, 20, 20, 200)
        column.IfcType = "Column"
        column = ifc_tools.aggregate(column, storey)
        beam = Arch.makeStructure(None, 20, 200, 20)
        beam.IfcType = "Beam"
        beam = ifc_tools.aggregate(beam, storey)
        rect = Draft.makeRectangle(200, 200)
        slab = Arch.makeStructure(rect, height=20)
        slab.IfcType = "Slab"
        slab = ifc_tools.aggregate(slab, storey)
        # TODO create door, window
        fco = len(FreeCAD.getDocument("IfcTest").Objects)
        ifco = len(proj.Proxy.ifcfile.by_type("IfcRoot"))
        print(ifco, "IFC objects created")
        self.assertTrue(fco == 8 - SDU and ifco == 12, "CreateDocument failed")

    def test10_ChangePlacement(self):
        FreeCAD.Console.PrintMessage("10. NativeIFC Changing Placement...")
        clearObjects()
        fp = getIfcFilePath()
        ifc_import.insert(
            fp,
            "IfcTest",
            strategy=2,
            shapemode=1,
            switchwb=0,
            silent=True,
            singledoc=SINGLEDOC,
        )
        obj = FreeCAD.getDocument("IfcTest").getObject("IfcObject00" + str(4 - SDU))
        elem = ifc_tools.get_ifc_element(obj)
        obj.Placement.move(FreeCAD.Vector(100, 200, 300))
        new_plac = ifcopenshell.util.placement.get_local_placement(elem.ObjectPlacement)
        new_plac = str(new_plac).replace(" ", "").replace("\n", "")
        target = "[[1.0.0.100.][0.1.0.200.][0.0.1.300.][0.0.0.1.]]"
        self.assertTrue(new_plac == target, "ChangePlacement failed")

    def test11_ChangeGeometry(self):
        FreeCAD.Console.PrintMessage("11. NativeIFC Changing Geometry...")
        clearObjects()
        fp = getIfcFilePath()
        ifc_import.insert(
            fp,
            "IfcTest",
            strategy=2,
            shapemode=0,
            switchwb=0,
            silent=True,
            singledoc=SINGLEDOC,
        )
        obj = FreeCAD.getDocument("IfcTest").getObject("IfcObject004")
        ifc_geometry.add_geom_properties(obj)
        obj.ExtrusionDepth = "6000 mm"
        FreeCAD.getDocument("IfcTest").recompute()
        self.assertTrue(obj.Shape.Volume > 1500000, "ChangeGeometry failed")

    def test12_RemoveObject(self):
        from nativeifc import ifc_observer
        ifc_observer.add_observer()
        FreeCAD.Console.PrintMessage("12. NativeIFC Remove object...")
        clearObjects()
        fp = getIfcFilePath()
        ifc_import.insert(
            fp,
            "IfcTest",
            strategy=2,
            shapemode=0,
            switchwb=0,
            silent=True,
            singledoc=SINGLEDOC,
        )
        ifcfile = ifc_tools.get_ifcfile(FreeCAD.getDocument("IfcTest").Objects[-1])
        count1 = len(ifcfile.by_type("IfcProduct"))
        FreeCAD.getDocument("IfcTest").removeObject("IfcObject004")
        count2 = len(ifcfile.by_type("IfcProduct"))
        self.assertTrue(count2 < count1, "RemoveObject failed")

    def test13_Materials(self):
        FreeCAD.Console.PrintMessage("13. NativeIFC Materials...")
        clearObjects()
        fp = getIfcFilePath()
        ifc_import.insert(
            fp,
            "IfcTest",
            strategy=2,
            shapemode=0,
            switchwb=0,
            silent=True,
            singledoc=SINGLEDOC,
        )
        proj = FreeCAD.getDocument("IfcTest").Objects[0]
        ifc_materials.load_materials(proj)
        prod = FreeCAD.getDocument("IfcTest").getObject("IfcObject006")
        ifcfile = ifc_tools.get_ifcfile(prod)
        mats_before = ifcfile.by_type("IfcMaterialDefinition")
        mat = Arch.makeMaterial("Red")
        ifc_materials.set_material(mat, prod)
        elem = ifc_tools.get_ifc_element(prod)
        res = ifcopenshell.util.element.get_material(elem)
        mats_after = ifcfile.by_type("IfcMaterialDefinition")
        self.assertTrue(len(mats_after) == len(mats_before) + 1, "Materials failed")

    def test14_Layers(self):
        FreeCAD.Console.PrintMessage("14. NativeIFC Layers...")
        clearObjects()
        fp = getIfcFilePath()
        ifc_import.insert(
            fp,
            "IfcTest",
            strategy=2,
            shapemode=0,
            switchwb=0,
            silent=True,
            singledoc=SINGLEDOC,
        )
        proj = FreeCAD.getDocument("IfcTest").Objects[0]
        ifcfile = ifc_tools.get_ifcfile(proj)
        lays_before = ifcfile.by_type("IfcPresentationLayerAssignment")
        layer = ifc_layers.create_layer("My Layer", proj)
        prod = FreeCAD.getDocument("IfcTest").getObject("IfcObject006")
        ifc_layers.add_to_layer(prod, layer)
        lays_after = ifcfile.by_type("IfcPresentationLayerAssignment")
        self.assertTrue(len(lays_after) == len(lays_before) + 1, "Layers failed")

    def test15_Psets(self):
        FreeCAD.Console.PrintMessage("15. NativeIFC Psets...")
        clearObjects()
        fp = getIfcFilePath()
        ifc_import.insert(
            fp,
            "IfcTest",
            strategy=2,
            shapemode=0,
            switchwb=0,
            silent=True,
            singledoc=SINGLEDOC,
        )
        obj = FreeCAD.getDocument("IfcTest").getObject("IfcObject004")
        ifcfile = ifc_tools.get_ifcfile(obj)
        pset = ifc_psets.add_pset(obj, "Pset_Custom")
        ifc_psets.add_property(ifcfile, pset, "MyMessageToTheWorld", "Hello, World!")
        self.assertTrue(ifc_psets.has_psets(obj), "Psets failed")
