"""
This page is in the table of contents.
Jitter jitters the loop end position to a different place on each layer to prevent the a ridge from forming.
 
The jitter manual page is at:
http://www.bitsfrombytes.com/wiki/index.php?title=Skeinforge_Jitter
 
==Operation==
The default 'Activate Jitter' checkbox is on.  When it is on, the functions described below will work, when it is off, the functions will not be called.
 
==Settings==
===Jitter Over Perimeter Width===
Default is two.
 
Defines the amount the loop ends will be jittered over the perimeter width.  A high value means the loops will start all over the place and a low value means loops will start at roughly the same place on each layer.
 
==Examples==
The following examples jitter the file Screw Holder Bottom.stl.  The examples are run in a terminal in the folder which contains Screw Holder Bottom.stl and jitter.py.
 
 
> python jitter.py
This brings up the jitter dialog.
 
 
> python jitter.py Screw Holder Bottom.stl
The jitter tool is parsing the file:
Screw Holder Bottom.stl
..
The jitter tool has created the file:
.. Screw Holder Bottom_jitter.gcode
 
 
> python
Python 2.5.1 (r251:54863, Sep 22 2007, 01:43:31)
[GCC 4.2.1 (SUSE Linux)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import jitter
>>> jitter.main()
This brings up the jitter dialog.
 
 
>>> jitter.writeOutput( 'Screw Holder Bottom.stl' )
The jitter tool is parsing the file:
Screw Holder Bottom.stl
..
The jitter tool has created the file:
.. Screw Holder Bottom_jitter.gcode
 
"""
 
from __future__ import absolute_import
#Init has to be imported first because it has code to workaround the python bug where relative imports don't work if the module is imported as a main module.
import __init__
 
from skeinforge_tools import profile
from skeinforge_tools.meta_plugins import polyfile
from skeinforge_tools.skeinforge_utilities import consecution
from skeinforge_tools.skeinforge_utilities import euclidean
from skeinforge_tools.skeinforge_utilities import gcodec
from skeinforge_tools.skeinforge_utilities import intercircle
from skeinforge_tools.skeinforge_utilities import interpret
from skeinforge_tools.skeinforge_utilities import settings
import math
import sys
 
 
__author__ = "Enrique Perez (perez_enrique@yahoo.com)"
__date__ = "$Date: 2008/21/04 $"
__license__ = "GPL 3.0"
 
def getCraftedText( fileName, text, jitterRepository = None ):
	"Jitter a gcode linear move text."
	return getCraftedTextFromText( gcodec.getTextIfEmpty( fileName, text ), jitterRepository )
 
def getCraftedTextFromText( gcodeText, jitterRepository = None ):
	"Jitter a gcode linear move text."
	if gcodec.isProcedureDoneOrFileIsEmpty( gcodeText, 'jitter' ):
		return gcodeText
	if jitterRepository == None:
		jitterRepository = settings.getReadRepository( JitterRepository() )
	if not jitterRepository.activateJitter.value:
		return gcodeText
	return JitterSkein().getCraftedGcode( jitterRepository, gcodeText )
 
def getJitteredLoop( jitterDistance, jitterLoop ):
	"Get a jittered loop path."
	loopLength = euclidean.getPolygonLength( jitterLoop )
	lastLength = 0.0
	pointIndex = 0
	totalLength = 0.0
	jitterPosition = ( jitterDistance + 256.0 * loopLength ) % loopLength
	while totalLength < jitterPosition and pointIndex < len( jitterLoop ):
		firstPoint = jitterLoop[ pointIndex ]
		secondPoint  = jitterLoop[ ( pointIndex + 1 ) % len( jitterLoop ) ]
		pointIndex += 1
		lastLength = totalLength
		totalLength += abs( firstPoint - secondPoint )
	remainingLength = jitterPosition - lastLength
	pointIndex = pointIndex % len( jitterLoop )
	ultimateJitteredPoint = jitterLoop[ pointIndex ]
	penultimateJitteredPointIndex = ( pointIndex + len( jitterLoop ) - 1 ) % len( jitterLoop )
	penultimateJitteredPoint = jitterLoop[ penultimateJitteredPointIndex ]
	segment = ultimateJitteredPoint - penultimateJitteredPoint
	segmentLength = abs( segment )
	originalOffsetLoop = euclidean.getAroundLoop( pointIndex, pointIndex, jitterLoop )
	if segmentLength <= 0.0:
		return originalOffsetLoop
	newUltimatePoint = penultimateJitteredPoint + segment * remainingLength / segmentLength
	return [ newUltimatePoint ] + originalOffsetLoop
 
def getNewRepository():
	"Get the repository constructor."
	return JitterRepository()
 
def isLoopNumberEqual( betweenX, betweenXIndex, loopNumber ):
	"Determine if the loop number is equal."
	if betweenXIndex >= len( betweenX ):
		return False
	return betweenX[ betweenXIndex ].index == loopNumber
 
def writeOutput( fileName = '' ):
	"Jitter a gcode linear move file."
	fileName = interpret.getFirstTranslatorFileNameUnmodified( fileName )
	if fileName != '':
		consecution.writeChainTextWithNounMessage( fileName, 'jitter' )
 
 
class JitterRepository:
	"A class to handle the jitter settings."
	def __init__( self ):
		"Set the default settings, execute title & settings fileName."
		profile.addListsToCraftTypeRepository( 'skeinforge_tools.craft_plugins.jitter.html', self )
		self.fileNameInput = settings.FileNameInput().getFromFileName( interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File for Jitter', self, '' )
		self.openWikiManualHelpPage = settings.HelpPage().getOpenFromAbsolute( 'http://www.bitsfrombytes.com/wiki/index.php?title=Skeinforge_Jitter' )
		self.activateJitter = settings.BooleanSetting().getFromValue( 'Activate Jitter', self, True )
		self.jitterOverPerimeterWidth = settings.FloatSpin().getFromValue( 1.0, 'Jitter Over Perimeter Width (ratio):', self, 3.0, 2.0 )
		self.executeTitle = 'Jitter'
 
	def execute( self ):
		"Jitter button has been clicked."
		fileNames = polyfile.getFileOrDirectoryTypesUnmodifiedGcode( self.fileNameInput.value, interpret.getImportPluginFileNames(), self.fileNameInput.wasCancelled )
		for fileName in fileNames:
			writeOutput( fileName )
 
 
class JitterSkein:
	"A class to jitter a skein of extrusions."
	def __init__( self ):
		self.beforeLoopLocation = None
		self.distanceFeedRate = gcodec.DistanceFeedRate()
		self.feedRateMinute = None
		self.isLoopPerimeter = False
		self.layerGolden = 0.0
		self.lineIndex = 0
		self.lines = None
		self.loopPath = None
		self.oldLocation = None
		self.oldLoopLocationComplex = None
 
	def addGcodeFromThreadZ( self, thread, z ):
		"Add a gcode thread to the output."
		if len( thread ) > 0:
			self.addGcodeMovementZ( self.travelFeedRatePerMinute, thread[ 0 ], z )
		else:
			print( "zero length vertex positions array which was skipped over, this should never happen" )
		if len( thread ) < 2:
			return
		self.distanceFeedRate.addLine( 'M101' )
		self.addGcodePathZ( self.feedRateMinute, thread[ 1 : ], z )
 
	def addGcodeMovementZ( self, feedRateMinute, point, z ):
		"Add a movement to the output."
		if feedRateMinute == None:
			feedRateMinute = self.operatingFeedRatePerMinute
		self.distanceFeedRate.addGcodeMovementZWithFeedRate( feedRateMinute, point, z )
 
	def addGcodePathZ( self, feedRateMinute, path, z ):
		"Add a gcode path, without modifying the extruder, to the output."
		for point in path:
			self.addGcodeMovementZ( feedRateMinute, point, z )
 
	def addTailoredLoopPath( self ):
		"Add a clipped and jittered loop path."
		loop = self.loopPath.path[ : - 1 ]
		if self.beforeLoopLocation != None:
			if self.oldLoopLocationComplex != None:
				self.beforeLoopLocation = self.oldLoopLocationComplex
			perimeterHalfWidth = 0.5 * self.perimeterWidth
			loop = euclidean.getLoopStartingNearest( perimeterHalfWidth, self.beforeLoopLocation, loop )
		if self.layerJitter != 0.0 and self.oldLoopLocationComplex == None:
			loop = getJitteredLoop( self.layerJitter, loop )
			loop = euclidean.getAwayPoints( loop, 0.2 * self.perimeterWidth )
		self.loopPath.path = loop + [ loop[ 0 ] ]
		self.addGcodeFromThreadZ( self.loopPath.path, self.loopPath.z )
		self.oldLoopLocationComplex = loop[ 0 ]
		self.loopPath = None
 
	def getCraftedGcode( self, jitterRepository, gcodeText ):
		"Parse gcode text and store the jitter gcode."
		self.lines = gcodec.getTextLines( gcodeText )
		self.parseInitialization( jitterRepository )
		for self.lineIndex in xrange( self.lineIndex, len( self.lines ) ):
			line = self.lines[ self.lineIndex ]
			self.parseAddJitter( line )
		return self.distanceFeedRate.output.getvalue()
 
	def getLinearMove( self, line, splitLine ):
		"Add to loop path if this is a loop or path."
		location = gcodec.getLocationFromSplitLine( self.oldLocation, splitLine )
		self.feedRateMinute = gcodec.getFeedRateMinute( self.feedRateMinute, splitLine )
		if self.isLoopPerimeter:
			if self.isNextExtruderOn():
				self.loopPath = euclidean.PathZ( location.z )
				if self.oldLocation != None:
					self.beforeLoopLocation = self.oldLocation.dropAxis( 2 )
		self.oldLocation = location
		if self.loopPath == None:
			self.oldLoopLocationComplex = None
			return line
		self.loopPath.path.append( location.dropAxis( 2 ) )
		return ''
 
	def isNextExtruderOn( self ):
		"Determine if there is an extruder on command before a move command."
		line = self.lines[ self.lineIndex ]
		splitLine = gcodec.getSplitLineBeforeBracketSemicolon( line )
		for afterIndex in xrange( self.lineIndex + 1, len( self.lines ) ):
			line = self.lines[ afterIndex ]
			splitLine = gcodec.getSplitLineBeforeBracketSemicolon( line )
			firstWord = gcodec.getFirstWord( splitLine )
			if firstWord == 'G1' or firstWord == 'M103':
				return False
			elif firstWord == 'M101':
				return True
		return False
 
	def parseAddJitter( self, line ):
		"Parse a gcode line, jitter it and add it to the jitter skein."
		splitLine = gcodec.getSplitLineBeforeBracketSemicolon( line )
		if len( splitLine ) < 1:
			return
		firstWord = splitLine[ 0 ]
		if firstWord == 'G1':
			line = self.getLinearMove( line, splitLine )
		elif firstWord == 'M101':
			if self.loopPath != None:
				return
		elif firstWord == 'M103':
			self.isLoopPerimeter = False
			if self.loopPath != None:
				self.addTailoredLoopPath()
		elif firstWord == '(<layer>':
			self.layerGolden += 0.61803398874989479
			self.layerJitter = self.jitter * ( math.fmod( self.layerGolden, 1.0 ) - 0.5 )
		elif firstWord == '(<loop>' or firstWord == '(<perimeter>':
			self.isLoopPerimeter = True
		self.distanceFeedRate.addLine( line )
 
	def parseInitialization( self, jitterRepository ):
		"Parse gcode initialization and store the parameters."
		for self.lineIndex in xrange( len( self.lines ) ):
			line = self.lines[ self.lineIndex ]
			splitLine = gcodec.getSplitLineBeforeBracketSemicolon( line )
			firstWord = gcodec.getFirstWord( splitLine )
			self.distanceFeedRate.parseSplitLine( firstWord, splitLine )
			if firstWord == '(</extruderInitialization>)':
				self.distanceFeedRate.addLine( '(<procedureDone> jitter </procedureDone>)' )
				return
			elif firstWord == '(<operatingFeedRatePerSecond>':
				self.operatingFeedRatePerMinute = 60.0 * float( splitLine[ 1 ] )
			elif firstWord == '(<perimeterWidth>':
				self.perimeterWidth = float( splitLine[ 1 ] )
				self.jitter = jitterRepository.jitterOverPerimeterWidth.value * self.perimeterWidth
			elif firstWord == '(<travelFeedRatePerSecond>':
				self.travelFeedRatePerMinute = 60.0 * float( splitLine[ 1 ] )
			self.distanceFeedRate.addLine( line )
 
 
def main():
	"Display the jitter dialog."
	if len( sys.argv ) > 1:
		writeOutput( ' '.join( sys.argv[ 1 : ] ) )
	else:
		settings.startMainLoopFromConstructor( getNewRepository() )
 
if __name__ == "__main__":
	main()