# ***************************************************************************
# *   Copyright (c) 2020 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 Lesser General Public License (LGPL)    *
# *   as published by the Free Software Foundation; either version 2 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 Library 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                                                                   *
# *                                                                         *
# ***************************************************************************
"""FreeCAD IFC importer - Multicore version"""

from __future__ import print_function

import sys
import time
import os

import FreeCAD
import Draft
import Arch
import importIFC
import importIFCHelper
from FreeCAD import Base
import ArchIFC

# global dicts to store ifc object/freecad object relationships

layers = {} # ifcid : Draft_Layer
materials = {} #ifcid : Arch_Material
objects = {} #ifcid : Arch_Component
subs = {} #host_ifcid: [child_ifcid,...]
adds = {} #host_ifcid: [child_ifcid,...]
colors = {} # objname : (r,g,b)


def open(filename):

    "opens an IFC file in a new document"

    return insert(filename)


def insert(filename,docname=None,preferences=None):

    """imports the contents of an IFC file in the given document"""

    import ifcopenshell
    from ifcopenshell import geom

    # reset global values
    global layers
    global materials
    global objects
    global adds
    global subs
    layers = {}
    materials = {}
    objects = {}
    adds = {}
    subs = {}

    # statistics
    starttime = time.time() # in seconds
    filesize = os.path.getsize(filename) * 0.000001 # in megabytes
    print("Opening",filename+",",round(filesize,2),"Mb")

    # setup ifcopenshell
    if not preferences:
        preferences = importIFC.getPreferences()
    settings = ifcopenshell.geom.settings()
    settings.set(settings.USE_BREP_DATA,True)
    settings.set(settings.SEW_SHELLS,True)
    settings.set(settings.USE_WORLD_COORDS,True)
    if preferences['SEPARATE_OPENINGS']:
        settings.set(settings.DISABLE_OPENING_SUBTRACTIONS,True)
    if preferences['SPLIT_LAYERS'] and hasattr(settings,"APPLY_LAYERSETS"):
        settings.set(settings.APPLY_LAYERSETS,True)

    # setup document
    if not FreeCAD.ActiveDocument:
        if not docname:
            docname = os.path.splitext(os.path.basename(filename))[0]
        doc = FreeCAD.newDocument(docname)
        doc.Label = docname
        FreeCAD.setActiveDocument(doc.Name)

    # open the file
    ifcfile = ifcopenshell.open(filename)
    progressbar = Base.ProgressIndicator()
    productscount = len(ifcfile.by_type("IfcProduct"))
    progressbar.start("Importing "+str(productscount)+" products...",productscount)
    cores = preferences["MULTICORE"]
    iterator = ifcopenshell.geom.iterator(settings,ifcfile,cores)
    iterator.initialize()
    count = 0

    # process objects
    while True:
        item = iterator.get()
        if item:
            brep = item.geometry.brep_data
            ifcproduct = ifcfile.by_id(item.guid)
            obj = createProduct(ifcproduct,brep)
            progressbar.next(True)
            writeProgress(count,productscount,starttime)
            count += 1
        if not iterator.next():
            break

    # post-processing
    processRelationships()
    storeColorDict()

    # finished
    progressbar.stop()
    FreeCAD.ActiveDocument.recompute()
    endtime = round(time.time()-starttime,1)
    fs = round(filesize,1)
    ratio = int(endtime/filesize)
    endtime = "%02d:%02d" % (divmod(endtime, 60))
    writeProgress() # this cleans the line
    print("Finished importing",fs,"Mb in",endtime,"s, or",ratio,"s/Mb")
    return FreeCAD.ActiveDocument


def writeProgress(count=None,total=None,starttime=None):

    """write progress to console"""

    if not FreeCAD.GuiUp:
        if count is None:
            sys.stdout.write("\r")
            return
        r = count/total
        elapsed = round(time.time()-starttime,1)
        if r:
            rest = elapsed*((1-r)/r)
            eta = "%02d:%02d" % (divmod(rest, 60))
        else:
            eta = "--:--"
        hashes = '#'*int(r*10)+' '*int(10-r*10)
        fstring = '\rImporting '+str(total)+' products [{0}] {1}%, ETA: {2}'
        sys.stdout.write(fstring.format(hashes, int(r*100),eta))


def createProduct(ifcproduct,brep):

    """creates an Arch object from an IFC product"""

    import Part

    shape = Part.Shape()
    shape.importBrepFromString(brep,False)
    shape.scale(1000.0) # IfcOpenShell outputs in meters
    if ifcproduct.is_a("IfcSpace"):
        obj = Arch.makeSpace()
    else:
        obj = Arch.makeComponent()
    obj.Shape = shape
    objects[ifcproduct.id()] = obj
    setAttributes(obj,ifcproduct)
    setProperties(obj,ifcproduct)
    createLayer(obj,ifcproduct)
    createMaterial(obj,ifcproduct)
    createModelStructure(obj,ifcproduct)
    setRelationships(obj,ifcproduct)
    setColor(obj,ifcproduct)
    return obj


def setAttributes(obj,ifcproduct):

    """sets the IFC attributes of a component"""

    ifctype = ArchIFC.uncamel(ifcproduct.is_a())
    if ifcproduct.Name:
        obj.Label = ifcproduct.Name
    if ifctype in ArchIFC.IfcTypes:
        obj.IfcType = ifctype
    for attr in dir(ifcproduct):
        if attr in obj.PropertiesList:
            value = getattr(ifcproduct,attr)
            if value:
                try:
                    setattr(obj,attr,value)
                except Exception:
                    pass


def setProperties(obj,ifcproduct):

    """sets the IFC properties of a component"""

    props = obj.IfcProperties
    for prel in ifcproduct.IsDefinedBy:
        if prel.is_a("IfcRelDefinesByProperties"):
            pset = prel.RelatingPropertyDefinition
            if pset.is_a("IfcPropertySet"):
                for prop in pset.HasProperties:
                    if hasattr(prop,"NominalValue"):
                        propname = prop.Name+";;"+pset.Name
                        v = [p.strip("'") for p in str(prop.NominalValue).strip(")").split(")")]
                        propvalue = ";;".join(v)


def setColor(obj,ifcproduct):

    """sets the color of an object"""

    global colors

    color = importIFCHelper.getColorFromProduct(ifcproduct)
    colors[obj.Name] = color
    if FreeCAD.GuiUp and color:
        obj.ViewObject.ShapeColor = color[:3]


def createLayer(obj,ifcproduct):

    """sets the layer of a component"""

    global layers

    if ifcproduct.Representation:
        for rep in ifcproduct.Representation.Representations:
            for layer in rep.LayerAssignments:
                if not layer.id() in layers:
                    layers[layer.id()] = Draft.make_layer(layer.Name)
                layers[layer.id()].Proxy.addObject(layers[layer.id()],obj)


def createMaterial(obj,ifcproduct):

    """sets the material of a component"""

    global materials

    for association in ifcproduct.HasAssociations:
        if association.is_a("IfcRelAssociatesMaterial"):
            material = association.RelatingMaterial
            if material.is_a("IfcMaterialList"):
                material = material.Materials[0] # take the first one for now...
            if material.is_a("IfcMaterial"):
                if not material.id() in materials:
                    color = importIFCHelper.getColorFromMaterial(material)
                    materials[material.id()] = Arch.makeMaterial(material.Name,color=color)
                obj.Material = materials[material.id()]


def createModelStructure(obj,ifcobj):

    """sets the parent containers of an IFC object"""

    global objects

    for parent in importIFCHelper.getParents(ifcobj):
        if not parent.id() in objects:
            if parent.is_a("IfcProject"):
                parentobj = Arch.makeProject()
            elif parent.is_a("IfcSite"):
                parentobj = Arch.makeSite()
            else:
                parentobj = Arch.makeBuildingPart()
            setAttributes(parentobj,parent)
            setProperties(parentobj,parent)
            createModelStructure(parentobj,parent)
            objects[parent.id()] = parentobj
        if hasattr(objects[parent.id()].Proxy,"addObject"):
            objects[parent.id()].Proxy.addObject(objects[parent.id()],obj)


def setRelationships(obj,ifcobj):

    """sets additions/subtractions"""

    global adds
    global subs

    if hasattr(ifcobj,"HasOpenings") and ifcobj.HasOpenings:
        for rel in ifcobj.HasOpenings:
            subs.setdefault(ifcobj.id(),[]).append(rel.RelatedOpeningElement)

    # TODO: assemblies & booleans


def processRelationships():

    """process all stored relationships"""

    for dom in ((subs,"Subtractions"),(adds,"Additions")):
        for key,vals in dom[0].items():
            if key in objects:
                for val in vals:
                    if val in objects:
                        if hasattr(objects[key],dom[1]):
                            g = getattr(objects[key],dom[1])
                            g.append(val)
                            setattr(objects[key],dom[1],g)

def storeColorDict():

    """stores the color dictionary in the document Meta if non-GUI mode"""

    if colors and not FreeCAD.GuiUp:
        import json
        d = FreeCAD.ActiveDocument.Meta
        d["colordict"] = json.dumps(colors)
        FreeCAD.ActiveDocument.Meta = d
