#*****************************************************************************
#*   Copyright (c) 2019 furti <daniel.furtlehner@gmx.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                                                                     *
#*                                                                           *
#*****************************************************************************

# Fence functionality for the Arch Workbench

import math

import FreeCAD
import ArchComponent
import draftobjects.patharray as patharray

if FreeCAD.GuiUp:
    import FreeCADGui
    from PySide.QtCore import QT_TRANSLATE_NOOP
    import PySide.QtGui as QtGui
else:
    # \cond
    def translate(ctxt, txt):
        return txt

    def QT_TRANSLATE_NOOP(ctxt, txt):
        return txt
    # \endcond

EAST = FreeCAD.Vector(1, 0, 0)


class _Fence(ArchComponent.Component):
    def __init__(self, obj):

        ArchComponent.Component.__init__(self, obj)
        self.setProperties(obj)
        # Does a IfcType exist?
        # obj.IfcType = "Fence"
        obj.MoveWithHost = False

    def setProperties(self, obj):
        ArchComponent.Component.setProperties(self, obj)

        pl = obj.PropertiesList

        if not "Section" in pl:
            obj.addProperty("App::PropertyLink", "Section", "Fence", QT_TRANSLATE_NOOP(
                "App::Property", "A single section of the fence"))

        if not "Post" in pl:
            obj.addProperty("App::PropertyLink", "Post", "Fence", QT_TRANSLATE_NOOP(
                "App::Property", "A single fence post"))

        if not "Path" in pl:
            obj.addProperty("App::PropertyLink", "Path", "Fence", QT_TRANSLATE_NOOP(
                "App::Property", "The Path the fence should follow"))

        if not "NumberOfSections" in pl:
            obj.addProperty("App::PropertyInteger", "NumberOfSections", "Fence", QT_TRANSLATE_NOOP(
                "App::Property", "The number of sections the fence is built of"))
            obj.setEditorMode("NumberOfSections", 1)

        if not "NumberOfPosts" in pl:
            obj.addProperty("App::PropertyInteger", "NumberOfPosts", "Fence", QT_TRANSLATE_NOOP(
                "App::Property", "The number of posts used to build the fence"))
            obj.setEditorMode("NumberOfPosts", 1)

        self.Type = "Fence"

    def __getstate__(self):
        if hasattr(self, 'sectionFaceNumbers'):
            return (self.sectionFaceNumbers)

        return None

    def __setstate__(self, state):
        if state is not None and isinstance(state, tuple):
            self.sectionFaceNumbers = state[0]

        return None

    def execute(self, obj):
        import Part

        pathwire = self.calculatePathWire(obj)

        if not pathwire:
            FreeCAD.Console.PrintLog(
                "ArchFence.execute: path " + obj.Path.Name + " has no edges\n")

            return

        if not obj.Section:
            FreeCAD.Console.PrintLog(
                "ArchFence.execute: Section not set\n")

            return

        if not obj.Post:
            FreeCAD.Console.PrintLog(
                "ArchFence.execute: Post not set\n")

            return

        pathLength = pathwire.Length
        sectionLength = obj.Section.Shape.BoundBox.XMax
        postLength = obj.Post.Shape.BoundBox.XMax

        obj.NumberOfSections = self.calculateNumberOfSections(
            pathLength, sectionLength, postLength)
        obj.NumberOfPosts = obj.NumberOfSections + 1

        # We assume that the section was drawn in front view.
        # We have to rotate the shape down so that it is aligned
        # correctly by the algorithm later on
        downRotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), -90)

        postPlacements = self.calculatePostPlacements(
            obj, pathwire, downRotation)

        postShapes = self.calculatePosts(obj, postPlacements)
        sectionShapes, sectionFaceNumbers = self.calculateSections(
            obj, postPlacements, postLength, sectionLength)

        allShapes = []
        allShapes.extend(postShapes)
        allShapes.extend(sectionShapes)

        compound = Part.makeCompound(allShapes)

        self.sectionFaceNumbers = sectionFaceNumbers

        obj.Shape = compound

    def calculateNumberOfSections(self, pathLength, sectionLength, postLength):
        withoutLastPost = pathLength - postLength
        realSectionLength = sectionLength + postLength

        return math.ceil(withoutLastPost / realSectionLength)

    def calculatePostPlacements(self, obj, pathwire, rotation):
        postWidth = obj.Post.Shape.BoundBox.YMax

        # We want to center the posts on the path. So move them the half width in
        transformationVector = FreeCAD.Vector(0, - postWidth / 2, 0)

        placements = patharray.placements_on_path(rotation, pathwire,
                                                  obj.NumberOfSections + 1,
                                                  transformationVector, True)

        # The placement of the last object is always the second entry in the list.
        # So we move it to the end
        placements.append(placements.pop(1))

        return placements

    def calculatePosts(self, obj, postPlacements):
        posts = []

        for placement in postPlacements:
            postCopy = obj.Post.Shape.copy()
            postCopy.Placement = placement

            posts.append(postCopy)

        return posts

    def calculateSections(self, obj, postPlacements, postLength, sectionLength):
        import Part

        shapes = []

        # For the colorization algorithm we have to store the number of faces for each section
        # It is possible that a section is clipped. Then the number of faces is not equal to the
        # number of faces in the original section
        faceNumbers = []

        for i in range(obj.NumberOfSections):
            startPlacement = postPlacements[i]
            endPlacement = postPlacements[i + 1]

            sectionLine = Part.LineSegment(
                startPlacement.Base, endPlacement.Base)
            sectionBase = sectionLine.value(postLength)

            if startPlacement.Rotation.isSame(endPlacement.Rotation):
                sectionRotation = endPlacement.Rotation
            else:
                direction = endPlacement.Base.sub(startPlacement.Base)

                sectionRotation = FreeCAD.Rotation(EAST, direction)

            placement = FreeCAD.Placement()
            placement.Base = sectionBase
            placement.Rotation = sectionRotation

            sectionCopy = obj.Section.Shape.copy()

            if sectionLength > sectionLine.length():
                # Part.show(Part.Shape([sectionLine]), 'line')
                sectionCopy = self.clipSection(
                    sectionCopy, sectionLength, sectionLine.length() - postLength)

            sectionCopy.Placement = placement

            shapes.append(sectionCopy)
            faceNumbers.append(len(sectionCopy.Faces))

        return (shapes, faceNumbers)

    def clipSection(self, shape, length, clipLength):
        import Part

        boundBox = shape.BoundBox
        lengthToCut = length - clipLength
        halfLengthToCut = lengthToCut / 2

        leftBox = Part.makeBox(halfLengthToCut, boundBox.YMax + 1, boundBox.ZMax + 1,
                               FreeCAD.Vector(boundBox.XMin, boundBox.YMin, boundBox.ZMin))
        rightBox = Part.makeBox(halfLengthToCut, boundBox.YMax + 1, boundBox.ZMax + 1,
                                FreeCAD.Vector(boundBox.XMin + halfLengthToCut + clipLength, boundBox.YMin, boundBox.ZMin))

        newShape = shape.cut([leftBox, rightBox])
        newBoundBox = newShape.BoundBox

        newShape.translate(FreeCAD.Vector(-newBoundBox.XMin, 0, 0))

        return newShape.removeSplitter()

    def calculatePathWire(self, obj):
        if (hasattr(obj.Path.Shape, 'Wires') and obj.Path.Shape.Wires):
            return obj.Path.Shape.Wires[0]
        elif obj.Path.Shape.Edges:
            return Part.Wire(obj.Path.Shape.Edges)

        return None


class _ViewProviderFence(ArchComponent.ViewProviderComponent):

    "A View Provider for the Fence object"

    def __init__(self, vobj):
        ArchComponent.ViewProviderComponent.__init__(self, vobj)
        # setProperties of ArchComponent will be overwritten
        # thus setProperties from ArchComponent will be explicit called to get the properties
        ArchComponent.ViewProviderComponent.setProperties(self, vobj)
        self.setProperties(vobj)

    def setProperties(self, vobj):
        pl = vobj.PropertiesList

        if not "UseOriginalColors" in pl:
            vobj.addProperty("App::PropertyBool", "UseOriginalColors", "Fence", QT_TRANSLATE_NOOP(
                "App::Property", "When true, the fence will be colored like the original post and section."))

    def attach(self, vobj):
        self.setProperties(vobj)

        return super().attach(vobj)

    def getIcon(self):
        import Arch_rc

        return ":/icons/Arch_Fence_Tree.svg"

    def claimChildren(self):
        children = []

        if self.Object.Section:
            children.append(self.Object.Section)

        if self.Object.Post:
            children.append(self.Object.Post)

        if self.Object.Path:
            children.append(self.Object.Path)

        return children

    def updateData(self, obj, prop):
        colorProps = ["Shape", "Section", "Post", "Path"]

        if prop in colorProps:
            self.applyColors(obj)
        else:
            super().updateData(obj, prop)

    def onChanged(self, vobj, prop):
        if prop == "UseOriginalColors":
            self.applyColors(vobj.Object)
        else:
            super().onChanged(vobj, prop)

    def applyColors(self, obj):
        if not hasattr(obj.ViewObject, "UseOriginalColors") or not obj.ViewObject.UseOriginalColors:
            obj.ViewObject.DiffuseColor = [obj.ViewObject.ShapeColor]
        else:
            post = obj.Post
            section = obj.Section

            numberOfPostFaces = len(post.Shape.Faces)
            numberOfSectionFaces = len(section.Shape.Faces)

            if hasattr(obj.Proxy, 'sectionFaceNumbers'):
                sectionFaceNumbers = obj.Proxy.sectionFaceNumbers
            else:
                sectionFaceNumbers = [0]

            if numberOfPostFaces == 0 or sum(sectionFaceNumbers) == 0:
                return

            postColors = self.normalizeColors(post, numberOfPostFaces)
            defaultSectionColors = self.normalizeColors(
                section, numberOfSectionFaces)

            ownColors = []

            # At first all posts are added to the shape
            for i in range(obj.NumberOfPosts):
                ownColors.extend(postColors)

            # Next all sections are added
            for i in range(obj.NumberOfSections):
                actualSectionFaceCount = sectionFaceNumbers[i]

                if actualSectionFaceCount == numberOfSectionFaces:
                    ownColors.extend(defaultSectionColors)
                else:
                    ownColors.extend(self.normalizeColors(
                        section, actualSectionFaceCount))

            viewObject = obj.ViewObject
            viewObject.DiffuseColor = ownColors

    def normalizeColors(self, obj, numberOfFaces):
        colors = obj.ViewObject.DiffuseColor

        if obj.TypeId == 'PartDesign::Body':
            # When colorizing a PartDesign Body we have two options
            # 1. The whole body got a shape color, that means the tip has only a single diffuse color set
            #   so we use the shape color of the body
            # 2. "Set colors" was called on the tip and the individual faces where colorized.
            #   We use the diffuseColors of the tip in that case
            tipColors = obj.Tip.ViewObject.DiffuseColor

            if len(tipColors) > 1:
                colors = tipColors

        numberOfColors = len(colors)

        if numberOfColors == 1:
            return colors * numberOfFaces

        colorsToUse = colors.copy()

        if numberOfColors == numberOfFaces:
            return colorsToUse
        else:
            # It is possible, that we have less faces than colors when something got clipped.
            # Remove the unneeded colors at the beginning and end
            halfNumberOfFacesToRemove = (numberOfColors - numberOfFaces) / 2
            start = int(math.ceil(halfNumberOfFacesToRemove))
            end = start + numberOfFaces

            return colorsToUse[start:end]


class _CommandFence:
    "the Arch Fence command definition"

    def GetResources(self):
        return {'Pixmap': 'Arch_Fence',
                'MenuText': QT_TRANSLATE_NOOP("Arch_Fence", "Fence"),
                'ToolTip': QT_TRANSLATE_NOOP("Arch_Fence", "Creates a fence object from a selected section, post and path")}

    def IsActive(self):
        return not FreeCAD.ActiveDocument is None

    def Activated(self):
        sel = FreeCADGui.Selection.getSelection()

        if len(sel) != 3:
            QtGui.QMessageBox.information(QtGui.QApplication.activeWindow(
            ), 'Arch Fence selection', 'Select a section, post and path in exactly this order to build a fence.')

            return

        section = sel[0]
        post = sel[1]
        path = sel[2]

        makeFence(section, post, path)


def makeFence(section, post, path):
    obj = FreeCAD.ActiveDocument.addObject(
        'Part::FeaturePython', 'Fence')

    _Fence(obj)
    obj.Section = section
    obj.Post = post
    obj.Path = path

    if FreeCAD.GuiUp:
        _ViewProviderFence(obj.ViewObject)

        hide(section)
        hide(post)
        hide(path)

    FreeCAD.ActiveDocument.recompute()

    return obj


def hide(obj):
    if hasattr(obj, 'ViewObject') and obj.ViewObject:
        obj.ViewObject.Visibility = False


if FreeCAD.GuiUp:
    FreeCADGui.addCommand('Arch_Fence', _CommandFence())

if __name__ == '__main__':
    # For testing purposes. When someone runs the File as a macro a default fence will be generated
    import Part

    def buildSection():
        parts = []

        parts.append(Part.makeBox(
            2000, 50, 30, FreeCAD.Vector(0, 0, 1000 - 30)))
        parts.append(Part.makeBox(2000, 50, 30))
        parts.append(Part.makeBox(20, 20, 1000 -
                                  60, FreeCAD.Vector(0, 15, 30)))
        parts.append(Part.makeBox(20, 20, 1000 - 60,
                                  FreeCAD.Vector(1980, 15, 30)))

        for i in range(8):
            parts.append(Part.makeBox(20, 20, 1000 - 60,
                                      FreeCAD.Vector((2000.0 / 9 * (i + 1)) - 10, 15, 30)))

        Part.show(Part.makeCompound(parts), "Section")

        return FreeCAD.ActiveDocument.getObject('Section')

    def buildPath():
        sketch = FreeCAD.ActiveDocument.addObject(
            'Sketcher::SketchObject', 'Path')
        sketch.Placement = FreeCAD.Placement(
            FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(0, 0, 0, 1))

        sketch.addGeometry(Part.LineSegment(FreeCAD.Vector(
            0, 0, 0), FreeCAD.Vector(20000, 0, 0)), False)
        sketch.addGeometry(Part.LineSegment(FreeCAD.Vector(
            20000, 0, 0), FreeCAD.Vector(20000, 20000, 0)), False)

        return sketch

    def buildPost():
        post = Part.makeBox(100, 100, 1000, FreeCAD.Vector(0, 0, 0))

        Part.show(post, 'Post')

        return FreeCAD.ActiveDocument.getObject('Post')

    def colorizeFaces(o, color=(0.6, 0.0, 0.0, 0.0), faceIndizes=[2]):
        numberOfFaces = len(o.Shape.Faces)
        vo = o.ViewObject

        originalColors = vo.DiffuseColor

        if len(originalColors) == 1:
            newColors = originalColors * numberOfFaces
        else:
            newColors = originalColors.copy()

        for i in faceIndizes:
            newColors[i] = color

        vo.DiffuseColor = newColors

    section = buildSection()
    path = buildPath()
    post = buildPost()

    colorizeFaces(post)

    print(makeFence(section, post, path))

    # _CommandFence().Activated()
