# -*- coding: utf-8 -*-
# Author: Ruslan Krenzler.
# Date: 27 Januar 2018
# Create a cross-fitting.
# Version 0.3

import math
import csv
import os.path

from PySide import QtCore, QtGui
import FreeCAD
import Spreadsheet
import Sketcher
import Part

tu = FreeCAD.Units.parseQuantity

def GetMacroPath():
	param = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Macro")
	return param.GetString("MacroPath","")

# This is the path to the dimensions table. 
CSV_TABLE_PATH =  GetMacroPath()+"/cross.csv"
# It must contain unique values in the column "Name" and also, dimensions listened below.
DIMENSIONS_USED = ["POD", "PID", "POD1", "PID1", "G", "G1", "G2", "G3", "H", "H1", "H2", "H3", "M", "M1"]


# The value RELATIVE_EPSILON is used to slightly change the size of a subtracted part
# to prevent problems with boolean operations.
# This value does not change the appearance of part and can be large.
# If the original value is L then we often use the value L*(1+RELATIVE_EPSILON) instead.
# The relative deviation is then (L*(1+RELATIVE_EPSILON)-L)/L = RELATIVE_EPSILON.
# That is why the constant has "relative" in its name.
# On my version of freecad 0.16 The macro works even with RELATIVE_EPSILON = 0.0.
# Maybe there is no more problems with boolean operations.
RELATIVE_EPSILON = 0.1

class Cross:
	def __init__(self, document):
		self.document = document
		# Fill data with test values
		self.G = tu("3 in")
		self.G1 = tu("3 in")
		self.G2 = tu("3 in")
		self.G3 = tu("3 in")
		self.H = tu("4 in") # It is L/2 for symetrical cross. Why extra dimension in documentation?
		self.H1 = tu("5 in")
		self.H2 = tu("6 in")
		self.H3 = tu("7 in")
		self.PID = tu("2 in")
		self.PID1 = tu("1 in")
		self.POD = tu("3 in")
		self.POD1 = tu("2 in")
		self.M = tu("5 in")
		self.M1 = tu("4 in")

	@staticmethod
	def toSolid(document, part, name):
		"""Convert object to a solid.
			Basically those are commands, which FreeCAD runs when user converts a part to a solid.
		"""
		s = part.Shape.Faces
		s = Part.Solid(Part.Shell(s))
		o = document.addObject("Part::Feature", name)
		o.Label=name
		o.Shape=s
		return o
	@staticmethod
	def NestedObjects(group):
		res = []
		if group.OutList == []:
			res.append(group)
		else:
			# Append children first.
			for o in group.OutList:
				res += Cross.NestedObjects(o)
			res.append(group)
		return res

	def create(self, convertToSolid):
		L = self.H+self.H2
		vertical_outer_cylinder = self.document.addObject("Part::Cylinder","VerticalOuterCynlider")
		vertical_outer_cylinder.Radius = self.M1/2
		vertical_outer_cylinder.Height = self.H1+self.H3
		vertical_outer_cylinder.Placement.Base = App.Vector(0,0,-self.H3)
		vertical_inner_cylinder = self.document.addObject("Part::Cylinder","VerticalInnerCynlider")
		vertical_inner_cylinder.Radius = self.PID1/2
		vertical_inner_cylinder.Height = vertical_outer_cylinder.Height * (1+RELATIVE_EPSILON)
		vertical_inner_cylinder.Placement.Base = App.Vector(0,0,-self.H3 *(1+RELATIVE_EPSILON))

		horizontal_outer_cylinder = self.document.addObject("Part::Cylinder","HorizontalOuterCynlider")
		horizontal_outer_cylinder.Radius = self.M/2
		horizontal_outer_cylinder.Height = L
		# I do not understand the logic here. Why when I use GUI the vector is App.Vector(0,0,-L/2)
		# and with the macros it is App.Vector(-L/2,0,0). Differne systems?
		horizontal_outer_cylinder.Placement = App.Placement(App.Vector(-self.H,0,0), App.Rotation(App.Vector(0,1,0),90), App.Vector(0,0,0))
		horizontal_inner_cylinder = self.document.addObject("Part::Cylinder","HorizontalInnerCynlider")
		horizontal_inner_cylinder.Radius = self.PID/2
		horizontal_inner_cylinder.Height = L*(1+RELATIVE_EPSILON)
		horizontal_inner_cylinder.Placement = App.Placement(App.Vector(-self.H*(1+RELATIVE_EPSILON),0,0), App.Rotation(App.Vector(0,1,0),90), App.Vector(0,0,0))
		
		# Fuse outer parts to a cross, fuse inner parts to a cross, substract both parts
		outer_fusion = self.document.addObject("Part::MultiFuse","OuterCrossFusion")
		outer_fusion.Shapes = [vertical_outer_cylinder,horizontal_outer_cylinder]
		inner_fusion = self.document.addObject("Part::MultiFuse","InnerCrossFusion")
		inner_fusion.Shapes = [vertical_inner_cylinder,horizontal_inner_cylinder]
		basic_cross = self.document.addObject("Part::Cut","Cut")
		basic_cross.Base = outer_fusion
		basic_cross.Tool = inner_fusion
		
		# Remove place for sockets.
		socket_left = self.document.addObject("Part::Cylinder","SocketLeft")
		socket_left.Radius = self.POD /2
		socket_left.Height = (self.H-self.G)*(1+RELATIVE_EPSILON)
		socket_left.Placement = App.Placement(App.Vector(-socket_left.Height - self.G,0,0), App.Rotation(App.Vector(0,1,0),90), App.Vector(0,0,0))
#		socket_left.Placement = App.Placement(App.Vector(-(self.H-self.G),0,0), App.Rotation(App.Vector(0,1,0),90), App.Vector(0,0,0))
		
		socket_right = self.document.addObject("Part::Cylinder","SocketRight")
		socket_right.Radius = self.POD /2
		socket_right.Height = (self.H2-self.G2)*(1+RELATIVE_EPSILON)
		socket_right.Placement = App.Placement(App.Vector(self.G2,0,0), App.Rotation(App.Vector(0,1,0),90), App.Vector(0,0,0))
		
		socket_top = self.document.addObject("Part::Cylinder","SocketTop")
		socket_top.Radius = self.POD1 /2
		socket_top.Height = (self.H1 - self.G1)*(1+RELATIVE_EPSILON)
		socket_top.Placement.Base = App.Vector(0,0,self.G1)
		
		socket_bottom = self.document.addObject("Part::Cylinder","SocketBottom")
		socket_bottom.Radius = self.POD1 /2
		socket_bottom.Height = (self.H3 - self.G3)*(1+RELATIVE_EPSILON)
		socket_bottom.Placement.Base = App.Vector(0,0,-socket_bottom.Height-self.G3)

		sockets_fusion = self.document.addObject("Part::MultiFuse","Sockets")
		sockets_fusion.Shapes = [socket_left,socket_right,socket_top,socket_bottom]
		# Remove sockets from the basic cross
		cross = self.document.addObject("Part::Cut","Cross")
		cross.Base = basic_cross
		cross.Tool = sockets_fusion
		if convertToSolid:
			# Before making a solid, recompute documents. Otherwise there will be
			#    s = Part.Solid(Part.Shell(s))
			#    <class 'Part.OCCError'>: Shape is null
			# exception.
			self.document.recompute()
			# Now convert all parts to solid, and remove intermediate data.
			solid = self.toSolid(self.document, cross, "cross (solid)")
			# Remove previous (intermediate parts).
			parts = Cross.NestedObjects(cross)
			# Document.removeObjects can remove multple objects, when we use
			# parts directly. To prevent exceptions with deleted objects,
			# use the name list instead.
			names_to_remove = []
			for part in parts:
				if part.Name not in names_to_remove:
					names_to_remove.append(part.Name)
			for name in names_to_remove:
				print("Deleting temporary objects %s."%name)
				self.document.removeObject(name)
			return solid
		return cross

class CrossFromTable:
	"""Create a part with dimensions from a CSV table."""
	def __init__ (self, document, table):
		self.document = document
		self.table = table
	def create(self, partName, convertToSolid = True):
		cross = Cross(self.document)
		row = self.table.findPart(partName)
		if row is None:
			print("Part not found")
			return
		cross.G = tu(row["G"])
		cross.G1 = tu(row["G1"])
		cross.G2 = tu(row["G2"])
		cross.G3 = tu(row["G3"])
		cross.H = tu(row["H"])
		cross.H1 = tu(row["H1"])
		cross.H2 = tu(row["H2"])
		cross.H3 = tu(row["H3"])
		cross.PID = tu(row["PID"])
		cross.PID1 = tu(row["PID1"])
		cross.POD = tu(row["POD"])
		cross.POD1 = tu(row["POD1"])
		cross.M = tu(row["M"])
		cross.M1 = tu(row["M1"])

		part = cross.create(convertToSolid)
		part.Label = partName
		return part



class CsvTable:
	""" Read coupling dimensions from a csv file.
	one part of the column must be unique and contains a unique key.
	It is the column "Name".
	"""
	def __init__(self, mandatoryDims=[]):
		"""
		@param mandatoryDims: list of column names which must be presented in the CSV files apart
		the "Name" column
		"""
		self.headers = []
		self.data = []
		self.hasValidData = False
		self.mandatoryDims=mandatoryDims
	def load(self, filename):
		"""Load data from a CSV file."""
		self.hasValidData = False
		with open(filename, "r") as csvfile:
			csv_reader = csv.reader(csvfile, delimiter=',', quotechar='"')
			self.headers = csv_reader.next()
			# Fill the talble
			self.data = []
			names = []
			ni = self.headers.index("Name")
			for row in csv_reader:
				# Check if the name is unique
				name = row[ni]
				if name in names:
					print('Error: Not unique name "%s" found in %s'%(name, filename))
					exit(1)
				else:
					names.append(name)
				self.data.append(row)
			csvfile.close() # Should I close this file explicitely?
			self.hasValidData = self.hasNecessaryColumns()

	def hasNecessaryColumns(self):
		""" Check if the data contains all calumns required to create a coupling."""
		return all(h in self.headers for h in (self.mandatoryDims + ["Name"]))

	def findPart(self, name):
		"""Return first first raw with the particular part name as a dictionary."""
		# First find out the index of the column "Name".
		ci = self.headers.index("Name")
		# Search for the first appereance of the name in this column.
		for row in self.data:
			if row[ci] == name:
				# Convert row to dicionary.
				return dict(zip(self.headers, row))
		return None

	def getPartName(self, index):
		"""Return part name of a row with the index *index*."""
		ci = self.headers.index("Name")
		return self.data[index][ci]

class PartTableModel(QtCore.QAbstractTableModel): 
	def __init__(self, headers, data, parent=None, *args):
		self.headers = headers
		self.table_data = data
		QtCore.QAbstractTableModel.__init__(self, parent, *args) 
	
	def rowCount(self, parent): 
		return len(self.table_data) 
 
	def columnCount(self, parent):
		return len(self.headers) 
 
	def data(self, index, role):
		if not index.isValid(): 
			return None
		elif role != QtCore.Qt.DisplayRole: 
			return None
		return self.table_data[index.row()][index.column()] 

	def getPartName(self, rowIndex):
		name_index = self.headers.index("Name")
		return self.table_data[rowIndex][name_index]

	def getPartRowIndex(self, partName):
		""" Return row index of the part with name partName.
		:param :partName name of the part
		:return: index of the first row whose part name is equal to partName
				return -1 if no row find.
		"""
		name_index = self.headers.index("Name")
		for row_i in range(name_index, len(self.table_data)):
			if self.table_data[row_i][name_index] == partName:
				return row_i
		return -1
	def headerData(self, col, orientation, role):
		if orientation ==QtCore. Qt.Horizontal and role == QtCore.Qt.DisplayRole:
			return self.headers[col]
		return None

class MainDialog(QtGui.QDialog):
	QSETTINGS_APPLICATION = "OSE piping freecad macros"
	QSETTINGS_NAME = "tee user input"
	def __init__(self, table):
		super(MainDialog, self).__init__()
		self.table = table
		self.initUi()
	def initUi(self): 
		Dialog = self # Added 
		self.result = -1 
		self.setupUi(self)
		# Fill table with dimensions. 
		self.initTable()

		# Restore previous user input. Ignore exceptions to prevent this part
		# part of the code to prevent GUI from starting, once settings are broken.
		try:
			self.restoreInput()
		except Exception as e:
			print ("Could not restore old user input!")
			print(e)
		self.show()

# The following lines are from QtDesigner .ui-file processed by pyside-uic
# pyside-uic --indent=0 create-cross.ui -o tmp.py
#
# The file paths needs to be adjusted manually. For example
# self.label.setPixmap(QtGui.QPixmap( GetMacroPath()+"/cross-dimensions.png"))
# access datata in some special FreeCAD directory.
	def setupUi(self, Dialog):
		Dialog.setObjectName("Dialog")
		Dialog.resize(803, 666)
		self.verticalLayout = QtGui.QVBoxLayout(Dialog)
		self.verticalLayout.setObjectName("verticalLayout")
		self.checkBoxCreateSolid = QtGui.QCheckBox(Dialog)
		self.checkBoxCreateSolid.setChecked(True)
		self.checkBoxCreateSolid.setObjectName("checkBoxCreateSolid")
		self.verticalLayout.addWidget(self.checkBoxCreateSolid)
		self.tableViewParts = QtGui.QTableView(Dialog)
		self.tableViewParts.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
		self.tableViewParts.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
		self.tableViewParts.setObjectName("tableViewParts")
		self.verticalLayout.addWidget(self.tableViewParts)
		self.label_2 = QtGui.QLabel(Dialog)
		self.label_2.setTextFormat(QtCore.Qt.AutoText)
		self.label_2.setWordWrap(True)
		self.label_2.setObjectName("label_2")
		self.verticalLayout.addWidget(self.label_2)
		self.label = QtGui.QLabel(Dialog)
		self.label.setText("")
		self.label.setPixmap(QtGui.QPixmap(GetMacroPath()+"/cross-dimensions.png"))
		self.label.setAlignment(QtCore.Qt.AlignCenter)
		self.label.setObjectName("label")
		self.verticalLayout.addWidget(self.label)
		self.buttonBox = QtGui.QDialogButtonBox(Dialog)
		self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
		self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok)
		self.buttonBox.setObjectName("buttonBox")
		self.verticalLayout.addWidget(self.buttonBox)

		self.retranslateUi(Dialog)
		QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("accepted()"), Dialog.accept)
		QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("rejected()"), Dialog.reject)
		QtCore.QMetaObject.connectSlotsByName(Dialog)

	def retranslateUi(self, Dialog):
		Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "Create cross", None, QtGui.QApplication.UnicodeUTF8))
		self.checkBoxCreateSolid.setText(QtGui.QApplication.translate("Dialog", "Create Solid", None, QtGui.QApplication.UnicodeUTF8))
		self.label_2.setText(QtGui.QApplication.translate("Dialog", "<html><head/><body><p>Only dimensions used are: M, M1, G, G1, G2, G3, H1, H2, H3, POD, POD1, PID, PID2. All other dimensions are used for inromation.</p></body></html>", None, QtGui.QApplication.UnicodeUTF8))


	def initTable(self):
		# Read table data from CSV
		self.model = PartTableModel(self.table.headers, self.table.data)
		self.tableViewParts.setModel(self.model)
		
	def getSelectedPartName(self):
		sel = form.tableViewParts.selectionModel()
		if sel.isSelected:
			if len(sel.selectedRows())> 0:
				rowIndex = sel.selectedRows()[0].row()
				return self.model.getPartName(rowIndex)
		return None

	def selectPartByName(self, partName):
		"""Select first row with a part with a name partName."""
		if partName is not None:
			row_i = self.model.getPartRowIndex(partName)
			if row_i >= 0:
				self.tableViewParts.selectRow(row_i)

	def accept(self):
		"""User clicked OK"""
		# Update active document.  If there is none, show a warning message and do nothing.
		document = App.activeDocument()
		if document is not None:
		# Get suitable row from the the table.
			partName = self.getSelectedPartName()
			createSolid = self.checkBoxCreateSolid.isChecked()
			if partName is not None:
				cross = CrossFromTable(document, self.table)
				cross.create(partName, createSolid)
				document.recompute()
				# Save user input for the next dialog call.
				self.saveInput()
				# Call parent class.
				super(MainDialog, self).accept()
			else:
				msgBox = QtGui.QMessageBox()
				msgBox.setText("Select part")
				msgBox.exec_()
		else:
			text = "I have not found any active document were I can create a cross fitting.\n"\
				"Use menu File->New to create a new document first, "\
				"then try to create the cross fitting again."
			msgBox = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Creating of the cross fitting failed.", text)
			msgBox.exec_()
	def saveInput(self):
		"""Store user input for the next run."""
		settings = QtCore.QSettings(MainDialog.QSETTINGS_APPLICATION, MainDialog.QSETTINGS_NAME)
		check = self.checkBoxCreateSolid.checkState()
		settings.setValue("checkBoxCreateSolid", int(check))
		settings.setValue("LastSelectedPartName", self.getSelectedPartName())
		settings.sync()
	def restoreInput(self):
		settings = QtCore.QSettings(MainDialog.QSETTINGS_APPLICATION, MainDialog.QSETTINGS_NAME)
		checkState = QtCore.Qt.CheckState(int(settings.value("checkBoxCreateSolid")))
		self.checkBoxCreateSolid.setCheckState(checkState)
		self.selectPartByName(settings.value("LastSelectedPartName"))


def GuiCheckTable():
	# Check if the CSV file exists.
	if os.path.isfile(CSV_TABLE_PATH) == False:
		text = "This macro requires %s  but this file does not exist."%(CSV_TABLE_PATH)
		msgBox = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Creating of the tee failed.", text)
		msgBox.exec_()
		exit(1) # Error

	print("Trying to load CSV file with dimensions: %s"%CSV_TABLE_PATH) 
	table = CsvTable()
	table.load(CSV_TABLE_PATH)

	if table.hasValidData == False:
		text = 'Invalid %s.\n'\
			'It must contain columns %s.'%(CSV_TABLE_PATH, ", ".join(DIMENSIONS_USED))
		msgBox = QtGui.QMessageBox(QtGui.QMessageBox.Warning, "Creating of the tee failed.", text)
		msgBox.exec_()
		exit(1) # Error

# Before working with macros, try to load the dimension table.
# Test macros.
def TestCross():
	document = App.activeDocument()
	cross = Cross(document)
	cross.create(True)
	document.recompute()

# Test macro.
def TestTable():
	document = App.activeDocument()
	table = CsvTable(DIMENSIONS_USED)
	table.load(CSV_TABLE_PATH)
	cross = CrossFromTable(document, table)
	for i in range(0, len(table.data)):
		print("Selecting row %d"%i)
		partName = table.getPartName(i)
		print("Creating part %s"%partName)
		cross.create(partName, False)
		document.recompute()

# Create cross fitting.
#TestCross()
#TestTable()
table = CsvTable()
table.load(CSV_TABLE_PATH)
form = MainDialog(table)



