# -*- coding: utf-8 -*-
# Author: Ruslan Krenzler.
# Date: 2 December 2017
# Create a pipe elbow.
# Version 0.2

import math

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

# Here are dimensions used to create a elbow.
alpha = "60 deg"
r1 = "1 in"
r2 = "2 in"
r3 = "3 in"
len1 = "4 in"
len2 = "5 in"

tu = FreeCAD.Units.parseQuantity

# Add sketches for swept part (circle) into X-Y Plane.
document = App.activeDocument()

class Error(Exception):
    """Base class for exceptions in this module."""
    pass

class UnplausibleDimensions(Error):
    """Exception raised when dimensions are unplausible. For example if
	outer diameter is larger than the iner one.

    Attributes:
        message -- explanation of the error
    """

    def __init__(self, message):
        self.message = message


class Elbow:
	# This factor 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.
	RELATIVE_EPSILON = 1.1
	
	def __init__ (self, document):
		self.document = document
		# Set default values.
		self.alpha = "60 deg"
		self.r1 = "1 in"
		self.r2 = "2 in"
		self.r3 = "3 in"
		self.len1 = "4 in"
		self.len2 = "5 in"
	@staticmethod
	def createElbowCylinder(document, group, r_cyl, r_bent, alpha, len1, len2):
		"""Create a cylinder with a radius r_cyl1 with a base on X-Y plane
		and bent it by angle alpha along radius r_bent. Add streight cylinders at the ends

		Add all new created objects to a group.
		This should simplify the cleaning up of the intermediate parts.
	
		:param r_cyl: radius of the cylinder in Base.Quantity
		:param r_bent: radius of the path in Base.Quantity
		:param alpha: in Base.Quantity
		:param len1: length of the streight part 1
		:param len2: length of the streight part 2
		"""
		base_sketch = document.addObject('Sketcher::SketchObject','BaseSketch')
		base_sketch.Placement = App.Placement(App.Vector(0.000000,0.000000,0.000000),App.Rotation(0.000000,0.000000,0.000000,1.000000))

		# When adding a radius, do not forget to convert the units.
		base_sketch.addGeometry(Part.Circle(App.Vector(0.000000,0.000000,0),App.Vector(0,0,1),r_cyl),False)

		# Add sweeping part into X-Z plane.
		path_sketch = document.addObject('Sketcher::SketchObject','PathSketch')
		path_sketch.Placement = App.Placement(App.Vector(0.000000,0.000000,0.000000),App.Rotation(-0.707107,0.000000,0.000000,-0.707107))
		# Note the pskecth is rotatet, therefore y and z coordinates are exchanged.
		# Add a line in botto direction.
		rel_epsilon = 0.0
		line1 = Part.Line(App.Vector(0.000000,0.000000,0),App.Vector(-0.000000,-len1,0))
		path_sketch.addGeometry(line1,False)

		# Add arc part.
		start = (tu("pi rad") - alpha).getValueAs("rad")
		stop = tu("pi rad").getValueAs("rad")
		arc = Part.ArcOfCircle(Part.Circle(App.Vector(r_bent,0,0),App.Vector(0,0,1),r_bent),start, stop)
		path_sketch.addGeometry(arc,False)

		# Find coordinates of the right point of the arc.
		x1 = (1-math.cos(alpha.getValueAs("rad")))*r_bent
		z1 = math.sin(alpha.getValueAs("rad"))*r_bent

		x2 = x1 + math.cos((tu("pi/2 rad")-alpha).getValueAs("rad"))*len2
		z2 = z1 + math.sin((tu("pi/2 rad")-alpha).getValueAs("rad"))*len2
		# Draw a streight line for the right pipe.
		line2 = Part.Line(App.Vector(x1,z1,0),App.Vector(x2,z2,0))
		line2_geometry = path_sketch.addGeometry(line2,False)
		# Sweep the parts.
		sweep = document.addObject('Part::Sweep','Sweep')
		sweep.Sections=[base_sketch, ]
		sweep.Spine=(path_sketch,["Edge1", "Edge2", "Edge3"])
		sweep.Solid=True
		sweep.Frenet=False
		# Add all the objects to the group.
		group.addObject(base_sketch)
		group.addObject(path_sketch,)
		group.addObject(sweep)
		return sweep
	@staticmethod
	def createElbowPart(document, group, r1, r3, alpha, len1, len2):
		# Make the outer part a little bit smaller, in order to prevent
		# problems with union of the pipe parts.
#		corrected_outer_radius = tu(r3)*(1-Elbow.BENT_RELATIVE_EPSILON)
		corrected_outer_radius = tu(r3)
		outer_sweep = Elbow.createElbowCylinder(document, group, corrected_outer_radius, tu(r3), tu(alpha), tu(len1), tu(len2))
		inner_sweep = Elbow.createElbowCylinder(document, group, tu(r1), tu(r3), tu(alpha), 
				tu(len1)*(1+Elbow.RELATIVE_EPSILON), tu(len2)*(1+Elbow.RELATIVE_EPSILON))
		bent_cut = document.addObject("Part::Cut","BentCut")
		bent_cut.Base = outer_sweep
		bent_cut.Tool = inner_sweep
		group.addObject(bent_cut)
		return bent_cut
	@staticmethod
	def toSolid(document, part, name):
		"""Convert object to a solid.
			Basically those are commands, which FreeCAD runs when you convert a part to a solid.

			Important!: recompute document. Without this step. The faces may not yet exists.
			(Is my interpretation correct?)
			document.recompute()
		"""
		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 += Elbow.NestedObjects(o)
			res.append(group)
		return res
	
	def checkDimensions(self):
		if not ( (tu(self.alpha) > tu("0 deg")) and (tu(self.alpha) <= tu("180 deg")) ):
			raise UnplausibleDimensions("alpha %s must be within of range (0,180]"%self.alpha)
		if not ( tu(self.r1) > 0):
			raise UnplausibleDimensions("Radus r1=%s must be positive"%self.r1)
		if not ( tu(self.r2) >= tu(self.r1)):
			raise UnplausibleDimensions("Radus r2=%s must be greater than or equal to r1=%s"%(self.r2, self.r1))
		if not ( tu(self.r3) >= tu(self.r2)):
			raise UnplausibleDimensions("Radus r3=%s must be greater than to r2=%s"%(self.r3, self.r2))
		if not ( tu(self.len1) > 0):
			raise UnplausibleDimensions("Length len1=%s must be positive"%self.len1)
		if not ( tu(self.len2) > 0):
			raise UnplausibleDimensions("Length len2=%s must be positive"%self.len2)

		#	raise UnplausibleDimensions("alpha %s out of range (0,180]"%self.alpha)
		

#		self.r1 = "1 in"
#		self.r2 = "2 in"
#		self.r3 = "3 in"
#		self.len1 = "4 in"
#		self.len2 = "5 in"
	def create(self, convertToSolid = True):
		self.checkDimensions()
		"""Create elbow."""
		# Create new group to put all the temporal data.
		group = self.document.addObject("App::DocumentObjectGroup", "elbow group")
		# Create the bent part.
		bent_part = self.createElbowPart(self.document, group, self.r1, self.r3, self.alpha, self.len1, self.len2)
		# Remove cyliders from both ends.
		inner_cylinder1 = document.addObject("Part::Cylinder","InnerCylinder1")
		inner_cylinder1.Radius = self.r2
		inner_cylinder1.Height = tu(self.len1)*(1+Elbow.RELATIVE_EPSILON)
		inner_cylinder1.Placement.Base = App.Vector(0,0, -inner_cylinder1.Height)

		inner_cylinder2 = document.addObject("Part::Cylinder","InnerCylinder2")
		inner_cylinder2.Radius = self.r2
		inner_cylinder2.Height = tu(self.len2)*(1+Elbow.RELATIVE_EPSILON)
		inner_cylinder2.Placement.Base = App.Vector(0,0, -inner_cylinder2.Height)
		x = (1-math.cos(tu(self.alpha).getValueAs("rad"))) *tu(self.r3)
		z = math.sin(tu(self.alpha).getValueAs("rad"))*tu(self.r3)
		inner_cylinder2.Placement = App.Placement(App.Vector(x,0,z),App.Rotation(App.Vector(0,1,0),tu(self.alpha)))
		cut1 = self.document.addObject("Part::Cut","PipeCut1") 
		cut1.Base = bent_part
		cut1.Tool = inner_cylinder1
		cut2 = self.document.addObject("Part::Cut","PipeCut2") 
		cut2.Base = cut1
		cut2.Tool = inner_cylinder2

		group.addObject(cut2)
		if convertToSolid:
			# Remove objects of the pipes separately.
			# Create all faces.
			document.recompute() # Should I really recompute all the document?
			# Now convert all parts to solid, and remove intermediate data.
			elbow_solid = self.toSolid(document, cut2, "elbow (solid)")
			# Remove previous (intermediate parts).
			parts = Elbow.NestedObjects(group)
			# 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)
				document.removeObject(name)
			return elbow_solid
		return group

NPS_NAME_COLUMN_INDEX = 0
NPS_SCHEDULE_COLUNN_INDEX = 1
NPS_OUTER_DIAMETER_COLUMN_INDEX = 2
NPS_INNER_DIAMETER_COLUMN_INDEX = 3

NPS_PVC_TABLE = [
	['1/8"', 40, 0.405,0.249,0.068,0.051,810],
	['1/4"', 40, 0.540,0.344,0.088, 0.086, 780],
	['3/8"', 40, 0.675, 0.473, 0.091, 0.115, 620],
	['1/2"', 40, 0.840, 0.602, 0.109, 0.170, 600],
	['3/4"', 40, 1.050, 0.804, 0.113, 0.226, 480],
	['1"', 40, 1.315, 1.029, 0.133, 0.333, 450],
	['1-1/4"', 40, 1.660, 1.360, 0.140, 0.450, 370],
	['1-1/2"', 40, 1.900, 1.590, 0.145, 0.537, 330],
	['2"', 40, 2.375, 2.047, 0.154, 0.720, 280],
	['2-1/2"', 40, 2.875, 2.445, 0.203, 1.136, 300],
	['3"', 40, 3.500, 3.042, 0.216, 1.488, 260],
	['3-1/2"', 40, 4.000, 3.521, 0.226, 1.789, 240],
	['4"', 40, 4.500, 3.998, 0.237, 2.118, 220],
	['5"', 40, 5.563, 5.016, 0.258, 2.874, 190],
	['6"', 40, 6.625, 6.031, 0.280, 3.733, 180],
	['8"', 40, 8.625, 7.942, 0.322, 5.619, 160],
	['10"', 40, 10.750, 9.976, 0.365, 7.966, 140],
	['12"', 40, 12.750, 11.889, 0.406, 10.534, 130],
	['1/8"',80, .405, .195, 0.095, 0.063, 1230],
	['1/4"',80, .540, .282, 0.119, 0.105, 1130],
	['3/8"',80, .675, .403, 0.126, 0.146, 920],
	['1/2"',80, .840, .526, 0.147, 0.213, 850],
	['3/4"',80, 1.050, .722, 0.154, 0.289, 690],
	['1"',80, 1.315, .936, 0.179, 0.424, 630],
	['1-1/4"',80, 1.660, 1.255, 0.191, 0.586, 520],
	['1-1/2"',80, 1.900, 1.476, 0.200, 0.711, 470],
	['2"',80, 2.375,1.913, 0.218, 0.984, 400],
	['2-1/2"',80, 2.875, 2.290, 0.276, 1.500, 420],
	['3"',80, 3.500, 2.864, 0.300, 2.010, 370],
	['3-1/2"',80, 4.000, 3.326, 0.318, 2.452, 350],
	['4"',80, 4.500, 3.786, 0.337, 2.938, 320],
	['5"',80, 5.563, 4.768, 0.375, 4.078, 290],
	['6"',80, 6.625, 5.709, 0.432, 5.610, 280],
	['8"',80, 8.625, 7.565, 0.500, 8.522, 250],
	['10"',80, 10.750, 9.493, 0.593, 12.635, 230],
	['12"',80 ,12.750, 11.294, 0.687	,17.384, 230]]

# The dimensions MGH are from https://www.aetnaplastics.com/site_media/media/attachments/aetna_product_aetnaproduct/204/PVC%20Sch%2040%20Fittings%20Dimensions.pdf
# Attention!: "1-13/16" in the table must be translated to "1+13.0/16" or to "1+13/16.0
ELBOW_90_NAME_COLUMN_INDEX = 0
ELBOW_90_PIPE_SIZE_NAME_COLUMN_INDEX = 1
ELBOW_90_SCHEDULE_COLUNN_INDEX = 2
ELBOW_90_G_COLUMN_INDEX = 3
ELBOW_90_H_COLUMN_INDEX = 4
ELBOW_90_M_COLUMN_INDEX = 5
ELBOW_90_HEADERS =["Part Number", "Size", "Schedule", "G", "H","M"]
ELBOW_90_TABLE=[['406-003','3/8"' , 40, 3/8.0, 1+1/8.0, 7/8.0],
['406-005','1/2"', 40, 1/2.0, 1+1/4.0, 1+1/8.0],
['406-007', '3/4"', 40, 9.0/16, 1+1.0/2,1+11/32.0],
['406-010', '1"', 40, 11/16.0, 1+13/16.0, 1+5/8.0],
['406-012', '1-1/4"', 40, 31/32.0, 2-5/32.0, 2],
['406-015','1-1/2"', 40, 1+1/16.0, 2+11/32.0,2+1/4.0],
['406-020', '2"', 40, 1+5/160, 2-11/16.0, 2+3/4.0],
['406-025','2-1/2"', 40, 1+1/2.0, 3+1/4.0, 3+11/32.0],
['406-030', '3"', 40, 1+27/32.0, 3+23/32.0, 4],
['406-040', '4"', 40, 2+13/32.0, 4+13/32.0, 5+1/32.0],
#['406-045F', '4-1/2"', 40, 8+5/8.0, 6+1/8.0, 5+7/16.0], # Impossible values. G>H.
['406-050', '5"', 40, 3+1/16.0, 6+1/8.0, 6+5/32.0],
['406-060','6"', 40, 3+17/32.0, 6+9/16.0, 7+9/32.0],
['406-080', '8"', 40, 4+13/32.0, 8+7/16.0, 9+3/8.0],
['406-100', '10"', 40, 5+13/16.0, 10+27/32.0, 11+19/32.0],
['406-100F', '10"', 40, 9+13/16.0, 15+1/16.0, 11+1/2.0],
['406-120', '12"', 40, 7+1/16.0, 13+9/16.0, 14+1/4.0],
['406-120F', '12"', 40, 11+3/16.0, 17+7/16.0, 13+9/16.0]]
# For other entries, the pipe dimensions are missing.
#['406-140', '14"', 7-1/2 14-1/2 15-11/16], 
#['406-140F', '14"', 13-3/16 20-3/16 14-7/8],
#['406-160F', '16"', 15-1/4 23-1/4 17],
#['406-180F', '18"', 17-5/16 26-5/16 19-1/8],
#['406-200F', '20"', 19-13/16 29-13/16 22-1/16],
#['406-240F', '24"', 23-7/16 35-7/16 25-3/8 216.00]]

class Elbow90:
	def __init__ (self, document):
		self.document = document
		# Set default values.
		self.G = "10 mm"
		self.H = "20 mm"
		self.M = "20 mm"
		self.pipeOuterR = "8 mm"
		self.pipeInnerR = "6 mm"
	def create(self, convertToSolid = True):
		el = Elbow(document)
		el.alpha = "90 deg"
		el.r3 = str(tu(self.M)/2)
		el.r2 = self.pipeOuterR
		el.r1 = self.pipeInnerR
		el.len1 = str(tu(self.H)-tu(self.G))
		el.len2 = el.len1
		return el.create(convertToSolid)

# Create a part following from the dimensions from NPS_PVC_TABLE.
class Elbow90FromTable:
	def __init__ (self, document):
		self.document = document
	@staticmethod
	def findPipeDimensions(npsPipeSizeName, schedule):
		"""Returns single row or None"""
		for row in NPS_PVC_TABLE:
			if row[NPS_NAME_COLUMN_INDEX]==npsPipeSizeName	and row[NPS_SCHEDULE_COLUNN_INDEX]==schedule:
				return row
		return None

	@staticmethod
	def findPart(partName):
		for row in ELBOW_90_TABLE:
			if row[ELBOW_90_NAME_COLUMN_INDEX]==partName:
				return row
		return row

	def create(self, convertToSolid = True):
		el90 = Elbow90(self.document)
		row = Elbow90FromTable.findPart(self.partName)
		if row is None:
			print("Part not found")
			return
		el90.G = '%f in'%row[ELBOW_90_G_COLUMN_INDEX]
		el90.H = '%f in'%row[ELBOW_90_H_COLUMN_INDEX]
		el90.M = '%f in'%row[ELBOW_90_M_COLUMN_INDEX]
		# get pipe dimensions
		pipe_dims =Elbow90FromTable.findPipeDimensions(row[ELBOW_90_PIPE_SIZE_NAME_COLUMN_INDEX],row[ELBOW_90_SCHEDULE_COLUNN_INDEX])

		if pipe_dims is None:
			print("No pipe dimensions found")
			return

		el90.pipeOuterR = str(pipe_dims[NPS_OUTER_DIAMETER_COLUMN_INDEX]/2.0)+'"'
		el90.pipeInnerR = str(pipe_dims[NPS_INNER_DIAMETER_COLUMN_INDEX]/2.0)+'"'
		return el90.create(convertToSolid)


# Create multiple parts for testing. It is computation intensive.
def Test():
	ell = Elbow(document)
	x = tu("0 in")
	x_step = tu("20 in")
	# Create multiple parts for test purpose
	for alpha_deg in [30,45,60,90,110]:
		alpha = tu("%d deg"%alpha_deg)
		y = tu("0 in")
		for r3_in in range(1,10):
			r3 = tu("%d in"%r3_in)
			y = y + r3 # Move to the next position (part I).
			r2 = r3*2/3
			r1 = r3/3
			ell.alpha = str(alpha)
			ell.r3 = str(r3)
			ell.r2 = str(r2)
			ell.r1 = str(r1)
			label = "Angle: %s, Outer radius: %s"%(alpha.getUserPreferred()[0], r3.getUserPreferred()[0])
			print("Creating... "+ label)
			part = ell.create(True) # Create a solid.
			#Add corresponding label to the part.
			part.Label = label
			Draft.move(part,FreeCAD.Vector(x,y,0),copy=False)
			y = y + r3 # Move to the next position (part II).
		x = x + x_step

class CreateNpsPvcElbowGuiClass(QtGui.QDialog):
	"""Return index of the row in the NPC_PVC library.
		Return -1 if nothing is selected.
	"""
	def __init__(self):
		super(CreateNpsPvcElbowGuiClass, self).__init__()
		self.initUi()
	def initUi(self): 
		Dialog = self # Added 
		self.result = -1 
		self.setupUi(self)
		# Fill table with dimensions.
		self.initTable()
		self.show()

# The next lines are from QtDesigner .ui-file processed by pyside-uic
	def setupUi(self, Dialog):
		Dialog.setObjectName("Dialog")
		Dialog.resize(710, 298)
		self.buttonBox = QtGui.QDialogButtonBox(Dialog)
		self.buttonBox.setGeometry(QtCore.QRect(390, 260, 301, 32))
		self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
		self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok)
		self.buttonBox.setObjectName("buttonBox")
		self.tableViewParts = QtGui.QTableView(Dialog)
		self.tableViewParts.setGeometry(QtCore.QRect(10, 60, 691, 192))
		self.tableViewParts.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)
		self.tableViewParts.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows)
		self.tableViewParts.setObjectName("tableViewParts")
		self.checkBoxCreateSolid = QtGui.QCheckBox(Dialog)
		self.checkBoxCreateSolid.setGeometry(QtCore.QRect(10, 30, 171, 22))
		self.checkBoxCreateSolid.setChecked(True)
		self.checkBoxCreateSolid.setObjectName("checkBoxCreateSolid")

		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 90° elbow", None, QtGui.QApplication.UnicodeUTF8))
		self.checkBoxCreateSolid.setText(QtGui.QApplication.translate("Dialog", "Create Solid", None, QtGui.QApplication.UnicodeUTF8))

	def initTable(self):
		model = PartTableModel()
		self.tableViewParts.setModel(model)
		#self.tableViewParts.setModel(ELBOW_90_TABLE)
		#for row in ELBOW_90_TABLE:
		#	self.tableViewParts.addItem(name)

	def getSelectedName(self):
		sel = form.tableViewParts.selectionModel()
		if sel.isSelected:
			if len(sel.selectedRows())> 0:
				rowIndex = sel.selectedRows()[0].row()
				#return model.getPartName(rowIndex) # This does not work. Why?
				return ELBOW_90_HEADERS[rowIndex][0]
		return None

	def accept(self):
		"""User clicked OK"""
		# Get suitable row from the the table.
		partName = self.getSelectedName()
		createSolid = self.checkBoxCreateSolid.isChecked()
		if partName is not None:
			#self.storeInput()
			el = Elbow90FromTable(document)
			el.partName = partName
			el.create(createSolid)
			# Call parent class.
			document.recompute()
			super(CreateNpsPvcElbowGuiClass, self).accept()
		else:
			msgBox = QtGui.QMessageBox()
			msgBox.setText("Select part")
			msgBox.exec_()


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

	def getPartName(self, rowIndex):
		return ELBOW_90_HEADERS[rowIndex][0]

	def headerData(self, col, orientation, role):
		if orientation ==QtCore. Qt.Horizontal and role == QtCore.Qt.DisplayRole:
			return ELBOW_90_HEADERS[col]
		return None

# Create all pipes from the 90°-elbow-table
def Test2():
	el = Elbow90FromTable(document)
	for row in ELBOW_90_TABLE:
		el.partName = row[ELBOW_90_NAME_COLUMN_INDEX]
		print("Creating part %s",el.partName)
		el.create(True)
		document.recompute()

# Show Dialog
#form = CreateNpsPvcElbowGuiClass()

#form.exec_() # What is it for?

#Test() # This needs a some time to calculate.
#Test2()# This needs a some time to calculate.

document.recompute()

