# ***************************************************************************
# *                                                                         *
# *   Copyright (c) 2013 - 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                                                                   *
# *                                                                         *
# ***************************************************************************


from __future__ import print_function
import FreeCAD
import FreeCADGui
from Material import getMaterialAttributeStructure
import os
from PySide import QtCore, QtGui
# from PySide import QtUiTools, QtSvg
import sys
if sys.version_info.major >= 3:
    unicode = str


__title__ = "FreeCAD material editor"
__author__ = "Yorik van Havre"
__url__ = "http://www.freecadweb.org"


class MaterialEditor:

    def __init__(self, obj=None, prop=None, material=None):

        """Initializes, optionally with an object name and a material property
        name to edit, or directly with a material dictionary."""

        self.obj = obj
        self.prop = prop
        self.material = material
        self.customprops = []
        self.internalprops = []
        self.groups = []
        self.directory = FreeCAD.getResourceDir() + "Mod/Material"

        # load the UI file from the same directory as this script
        self.widget = FreeCADGui.PySideUic.loadUi(
            os.path.dirname(__file__) + os.sep + "materials-editor.ui"
        )

        # additional UI fixes and tweaks
        widget = self.widget
        buttonURL = widget.ButtonURL
        buttonDeleteProperty = widget.ButtonDeleteProperty
        buttonAddProperty = widget.ButtonAddProperty
        standardButtons = widget.standardButtons
        buttonOpen = widget.ButtonOpen
        buttonSave = widget.ButtonSave
        comboMaterial = widget.ComboMaterial
        treeView = widget.treeView

        # temporarily hide preview fields, as they are not used yet
        # TODO : implement previews
        widget.PreviewGroup.hide()

        buttonURL.setIcon(QtGui.QIcon(":/icons/internet-web-browser.svg"))
        buttonDeleteProperty.setEnabled(False)
        standardButtons.button(QtGui.QDialogButtonBox.Ok).setAutoDefault(False)
        standardButtons.button(QtGui.QDialogButtonBox.Cancel).setAutoDefault(False)
        self.updateCards()
        # TODO allow to enter a custom property by pressing Enter in the lineedit (currently closes the dialog)

        standardButtons.rejected.connect(self.reject)
        standardButtons.accepted.connect(self.accept)
        buttonOpen.clicked.connect(self.openfile)
        buttonSave.clicked.connect(self.savefile)
        buttonURL.clicked.connect(self.openProductURL)
        comboMaterial.currentIndexChanged[str].connect(self.updateContents)
        buttonAddProperty.clicked.connect(self.addCustomProperty)
        buttonDeleteProperty.clicked.connect(self.deleteCustomProperty)
        treeView.clicked.connect(self.checkDeletable)

        model = QtGui.QStandardItemModel()
        treeView.setModel(model)
        treeView.setUniformRowHeights(True)
        treeView.setItemDelegate(MaterialsDelegate())

        # update the editor with the contents of the property, if we have one
        d = None
        if self.prop and self.obj:
            d = FreeCAD.ActiveDocument.getObject(self.obj).getPropertyByName(self.prop)
        elif self.material:
            d = self.material

        self.implementModel()
        if d:
            self.updateContents(d)

    def implementModel(self):

        '''implements the model with the material attribute structure.'''

        widget = self.widget
        treeView = widget.treeView
        model = treeView.model()
        model.setHorizontalHeaderLabels(["Property", "Value", "Type"])

        treeView.setColumnWidth(0, 250)
        treeView.setColumnWidth(1, 250)
        treeView.setColumnHidden(2, True)

        tree = getMaterialAttributeStructure(True)
        MatPropDict = tree.getroot()

        for group in MatPropDict.getchildren():
            gg = group.attrib['Name']
            top = QtGui.QStandardItem(gg)
            model.appendRow([top])
            self.groups.append(gg)

            for proper in group.getchildren():
                properDict = proper.attrib

                pp = properDict['Name']
                item = QtGui.QStandardItem(pp)
                self.internalprops.append(pp)

                it = QtGui.QStandardItem()

                tt = properDict['Type']
                itType = QtGui.QStandardItem(tt)

                top.appendRow([item, it, itType])

            top.sortChildren(0)

        treeView.expandAll()

    def updateContents(self, data):

        '''updates the contents of the editor with the given data, can be:
           - the name of a card, if material is changed in editors combo box
           - a dictionary, if the editor was called with data.'''

        if isinstance(data, dict):
            # a standard material property dict is provided
            model = self.widget.treeView.model()
            root = model.invisibleRootItem()
            for gg in range(root.rowCount() - 1):
                group = root.child(gg, 0)
                for pp in range(group.rowCount()):
                    item = group.child(pp, 0)
                    it = group.child(pp, 1)
                    kk = self.collapseKey(item.text())

                    try:
                        value = data[kk]
                        it.setText(value)
                        del data[kk]
                    except KeyError:
                        it.setText("")

            userGroup = root.child(gg + 1, 0)
            userGroup.setRowCount(0)
            self.customprops = []

            for k, i in data.items():
                k = self.expandKey(k)
                item = QtGui.QStandardItem(k)
                it = QtGui.QStandardItem(i)
                userGroup.appendRow([item, it])
                self.customprops.append(k)

        elif isinstance(data, unicode):
            # a card name is provided, search card, read material data and call this def once more with std material property dict
            k = str(data)
            if k:
                if k in self.cards:

                    import importFCMat
                    d = importFCMat.read(self.cards[k])
                    if d:
                        self.updateContents(d)

    def getMaterialResources(self):
        self.fem_prefs = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Material/Resources")
        use_built_in_materials = self.fem_prefs.GetBool("UseBuiltInMaterials", True)
        use_mat_from_config_dir = self.fem_prefs.GetBool("UseMaterialsFromConfigDir", True)
        use_mat_from_custom_dir = self.fem_prefs.GetBool("UseMaterialsFromCustomDir", True)
        if use_mat_from_custom_dir:
            custom_mat_dir = self.fem_prefs.GetString("CustomMaterialsDir", "")
        # later found cards with same name will override cards
        # FreeCAD returns paths with / at the end, thus not os.sep is needed on first +
        self.resources = []
        if use_built_in_materials:
            self.resources.append(FreeCAD.getResourceDir() + "Mod" + os.sep + "Material" + os.sep + "StandardMaterial")
        if use_mat_from_config_dir:
            self.resources.append(FreeCAD.ConfigGet("UserAppData") + "Material")
        if use_mat_from_custom_dir:
            custom_mat_dir = self.fem_prefs.GetString("CustomMaterialsDir", "")
            if os.path.exists(custom_mat_dir):
                self.resources.append(custom_mat_dir)
        self.outputResources()

    def outputResources(self):
        print('locations to look for material cards:')
        for path in self.resources:
            print('  ' + path)
        print('\n')

    def outputCards(self):
        print('material cards:')
        for card in self.cards:
            print('  ' + card + ': ' + self.cards[card])
        print('\n')

    def updateCards(self):
        "updates the contents of the materials combo with existing material cards"
        self.getMaterialResources()
        self.cards = {}
        for p in self.resources:
            if os.path.exists(p):
                for f in sorted(os.listdir(p)):
                    b, e = os.path.splitext(f)
                    if e.upper() == ".FCMAT":
                        self.cards[b] = p + os.sep + f
        # self.outputCards()
        if self.cards:
            self.widget.ComboMaterial.clear()
            self.widget.ComboMaterial.addItem("")  # add a blank item first
            for k, i in self.cards.items():
                self.widget.ComboMaterial.addItem(k)

    def openProductURL(self):
        "opens the contents of the ProductURL field in an external browser."

        model = self.widget.treeView.model()
        item = model.findItems(translate("Material", "Product URL"),
                               QtCore.Qt.MatchRecursive, 0)[0]
        group = item.parent()
        it = group.child(item.row(), 1)
        url = it.text()
        if url:
            QtGui.QDesktopServices.openUrl(QtCore.QUrl(url, QtCore.QUrl.TolerantMode))

    def accept(self):
        ""

        QtGui.QDialog.accept(self.widget)

    def reject(self):
        ""

        QtGui.QDialog.reject(self.widget)

    def expandKey(self, key):
        "adds spaces before caps in a KeyName"
        nk = ""
        for ln in key:
            if ln.isupper():
                if nk:
                    # this allows for series of caps, such as ProductURL
                    if not nk[-1].isupper():
                        nk += " "
            nk += ln
        return nk

    def collapseKey(self, key):
        "removes the spaces in a Key Name"
        nk = ""
        for ln in key:
            if ln != " ":
                nk += ln
        return nk

    def addCustomProperty(self, key=None, value=None):
        "Adds a custom property to the editor, optionally with a value."

        if not key:
            key = self.widget.EditProperty.text()

        if key:
            model = self.widget.treeView.model()
            item = model.findItems(key, QtCore.Qt.MatchRecursive, 0)
            if not item:

                top = model.findItems(translate("Material", "User defined"),
                                      QtCore.Qt.MatchExactly, 0)[0]
                item = QtGui.QStandardItem(key)
                it = QtGui.QStandardItem(value)
                top.appendRow([item, it])
                self.customprops.append(key)

    def deleteCustomProperty(self, key=None):

        '''Deletes a custom property from the editor,
        or deletes the value of an internal property.'''

        widget = self.widget
        treeView = widget.treeView
        model = treeView.model()
        buttonDeleteProperty = widget.ButtonDeleteProperty

        if not key:

            index = treeView.selectedIndexes()[0]
            item = model.itemFromIndex(index)
            key = item.text()

        if key:
            item = model.findItems(key, QtCore.Qt.MatchRecursive, 0)
            if item:

                index = model.indexFromItem(item[0])
                topIndex = index.parent()
                top = model.itemFromIndex(topIndex)
                row = item[0].row()

                if key in self.customprops:
                    top.takeRow(row)
                    self.customprops.remove(key)
                    buttonDeleteProperty.setProperty("text", "Delete property")

                elif key in self.internalprops:
                    it = top.child(row, 1)
                    it.setText("")
                    buttonDeleteProperty.setProperty("text", "Delete value")

        buttonDeleteProperty.setEnabled(False)

    def checkDeletable(self, index):

        '''Checks if the current item is a custom or an internal property,
        and enable the delete property or delete value button.'''

        widget = self.widget
        buttonDeleteProperty = widget.ButtonDeleteProperty
        treeView = widget.treeView
        model = treeView.model()
        ind = treeView.selectedIndexes()[0]
        item = model.itemFromIndex(ind)
        text = item.text()

        if text in self.customprops:
            buttonDeleteProperty.setEnabled(True)
            buttonDeleteProperty.setProperty("text", "Delete property")

        elif text in self.internalprops:
            indParent = ind.parent()
            group = model.itemFromIndex(indParent)
            row = item.row()
            it = group.child(row, 1)
            buttonDeleteProperty.setProperty("text", "Delete value")
            if it.text():
                buttonDeleteProperty.setEnabled(True)
            else:
                buttonDeleteProperty.setEnabled(False)

        else:
            buttonDeleteProperty.setEnabled(False)
            buttonDeleteProperty.setProperty("text", "Delete property")

    def getDict(self):
        "returns a dictionary from the contents of the editor."

        model = self.widget.treeView.model()
        root = model.invisibleRootItem()

        d = {}
        for gg in range(root.rowCount()):
            group = root.child(gg)
            for row in range(group.rowCount()):
                kk = group.child(row, 0).text()
                ii = group.child(row, 1).text()

                # TODO the following should be translated back to english,since text(0) could be translated
                matkey = self.collapseKey(str(kk))
                matvalue = unicode(ii)
                if matvalue or (matkey == 'Name'):
                    # use only keys which are not empty and the name even if empty
                    d[matkey] = matvalue
        # self.outputDict(d)
        return d

    def outputDict(self, d):
        print('MaterialEditor dictionary')
        for param in d:
            print('  ' + param + ' : ' + d[param])

    '''def setTexture(self, pattern):
        "displays a texture preview if needed"
        self.widget.PreviewVector.hide()
        if pattern:
            try:
                import DrawingPatterns
            except:
                print("DrawingPatterns not found")
            else:
                pattern = DrawingPatterns.buildFileSwatch(pattern, size=96, png=True)
                if pattern:
                    self.widget.PreviewVector.setPixmap(QtGui.QPixmap(pattern))
                    self.widget.PreviewVector.show()'''

    def openfile(self):
        "Opens a FCMat file"
        filetuple = QtGui.QFileDialog.getOpenFileName(QtGui.QApplication.activeWindow(), 'Open FreeCAD Material file', self.directory, '*.FCMat')
        filename = filetuple[0]  # a tuple of two empty strings returns True, so use the filename directly
        if filename:
            import importFCMat
            self.directory = os.path.dirname(filename)
            d = importFCMat.read(filename)
            if d:
                self.updateContents(d)

    def savefile(self):
        "Saves a FCMat file."

        model = self.widget.treeView.model()
        item = model.findItems(translate("Material", "Name"),
                               QtCore.Qt.MatchRecursive, 0)[0]
        group = item.parent()
        it = group.child(item.row(), 1)
        name = it.text()
        if sys.version_info.major < 3:
            if isinstance(name, unicode):
                name = name.encode("utf8")
        if not name:
            name = "Material"
        filetuple =\
            QtGui.QFileDialog.getSaveFileName(QtGui.QApplication.activeWindow(),
                                              'Save FreeCAD Material file',
                                              self.directory + '/' + name + '.FCMat', '*.FCMat')
        filename = filetuple[0]  # a tuple of two empty strings returns True, so use the filename directly
        if filename:
            self.directory = os.path.dirname(filename)
            d = self.getDict()
            # self.outputDict(d)
            if d:
                import importFCMat
                importFCMat.write(filename, d)
                self.updateCards()

    def show(self):
        return self.widget.show()

    def exec_(self):
        return self.widget.exec_()


class MaterialsDelegate(QtGui.QStyledItemDelegate):

    '''provides display and editing facilities for data items from a model.'''

    def __init__(self):
        ""

        super(MaterialsDelegate, self).__init__()

    def createEditor(self, parent, option, index):

        '''returns the widget used to change data from the model.'''

        model = index.model()
        column = index.column()

        item = model.itemFromIndex(index)
        group = item.parent()
        if not group:
            return

        if column == 1:

            row = index.row()

            PP = group.child(row, 0)
            matproperty = PP.text().replace(" ", "")  # remove spaces

            TT = group.child(row, 2)

            if TT:
                Type = TT.text()

            else:
                Type = "String"

            VV = group.child(row, 1)
            Value = VV.text()

            editor = matProperWidget(parent, matproperty, Type, Value)

        elif column == 0:

            if group.text() == "User defined":
                editor = matProperWidget(parent)

            else:
                return

        else:

            return

        return editor

    def setEditorData(self, editor, index):

        '''provides the widget with data to manipulate.'''

        Type = editor.property('Type')
        model = index.model()
        item = model.itemFromIndex(index)

        if Type == "Color":

            color = editor.property('color')
            color = color.getRgb()
            item.setText(str(color))

        elif Type == "File":

            lineEdit = editor.children()[1]
            item.setText(lineEdit.text())

        else:

            super(MaterialsDelegate, self).setEditorData(editor, index)


ui = FreeCADGui.UiLoader()


def matProperWidget(parent=None, matproperty=None, Type="String", Value=None,
                    minimum=None, maximum=None, stepsize=None, precision=None):

    '''customs widgets for the material stuff.'''

    if Type == "String":

        widget = ui.createWidget("Gui::PrefLineEdit")

    elif Type == "URL":

        widget = ui.createWidget("Gui::PrefLineEdit")

    elif Type == "File":

        widget = ui.createWidget("Gui::FileChooser")
        if Value:
            lineEdit = widget.children()[1]
            lineEdit.setText(Value)

    elif Type == "Quantity":

        widget = ui.createWidget("Gui::InputField")
        if hasattr(FreeCAD.Units, matproperty):
            unit = getattr(FreeCAD.Units, matproperty)
            quantity = FreeCAD.Units.Quantity(1, unit)
            widget.setProperty('unit', quantity.getUserPreferred()[2])
        else:
            FreeCAD.Console.PrintError('Not known unit for property: ' + matproperty + '\n')

    elif Type == "Integer":

        widget = ui.createWidget("Gui::UIntSpinBox")

    elif Type == "Float":

        widget = ui.createWidget("Gui::PrefDoubleSpinBox")

    elif Type == "Enumerator":

        widget = ui.createWidget("Gui::PrefComboBox")

    elif Type == "Boolean":

        widget = ui.createWidget("Gui::PrefComboBox")
        widget.insertItems(0, ['', 'False', 'True'])

    elif Type == "Vector":

        widget = ui.createWidget("Gui::PrefLineEdit")

    elif Type == "Color":

        widget = ui.createWidget("Gui::PrefColorButton")
        if Value:
            value = string2tuple(Value)
            color = QtGui.QColor()
            color.setRgb(value[0], value[1], value[2], value[3])
            widget.setProperty('color', color)

    else:

        widget = QtGui.QLineEdit()

    if minimum is not None:
        widget.setProperty('minimum', minimum)
    if maximum is not None:
        widget.setProperty('maximum', maximum)
    if stepsize is not None:
        widget.setProperty('stepsize', stepsize)
    if precision is not None:
        widget.setProperty('precision', precision)

    widget.setProperty('Type', Type)

    widget.setParent(parent)

    return widget


def string2tuple(string):
    "provisionally"
    value = string[1:-1]
    value = value.split(',')
    value = [int(v) for v in value]
    value = tuple(value)
    return value


def translate(context, text):
    "translates text"
    return text  # TODO use Qt translation mechanism here


def openEditor(obj=None, prop=None):
    """openEditor([obj,prop]): opens the editor, optionally with
    an object name and material property name to edit"""
    editor = MaterialEditor(obj, prop)
    editor.exec_()


def editMaterial(material):
    """editMaterial(material): opens the editor to edit the contents
    of the given material dictionary. Returns the modified material dictionary."""
    # if the material editor is opened with this def the combo box with the card name is empty
    # this makes sense, because the editor was not opened with a card but with material dictionary instead
    # TODO: add some text in combo box, may be "custom material data" or "user material data"
    # TODO: all card could be checked if one fits exact ALL provided data and than this card name could be displayed
    editor = MaterialEditor(material=material)
    result = editor.exec_()
    if result:
        return editor.getDict()
    else:
        return material


'''
# some examples how to open the material editor in Python:
import MaterialEditor
MaterialEditor.openEditor()

doc = FreeCAD.open(FreeCAD.ConfigGet("AppHomePath") + 'data/examples/FemCalculixCantilever3D.FCStd')
import MaterialEditor
MaterialEditor.openEditor('SolidMaterial', 'Material')

import MaterialEditor
MaterialEditor.editMaterial({'Density': '1234.0 kg/m^3', 'Name': 'My-Material-Data', 'PoissonRatio': '0.66', 'YoungsModulus': '123456 MPa'})

import MaterialEditor
MaterialEditor.editMaterial('ABS')

'''
