"""
Clip is a script to clip loop ends.

The default 'Activate Clip' checkbox is on.  When it is on, the functions described below will work, when it is off, the functions
will not be called.

Clip clips the ends of loops to prevent bumps from forming.  The "Clip Over Extrusion Width (ratio)" is the ratio of the amount
each end of the loop is clipped over the extrusion width.  The total gap will therefore be twice the clip.  If the ratio is too high
loops will have a gap, if the ratio is too low there will be a bulge at the loop ends.  To run clip, in a shell type:
> python clip.py

The following examples clip the files Screw Holder Bottom.gcode & Screw Holder Bottom.stl.  The examples are run in a terminal in the
folder which contains Screw Holder Bottom.gcode, Screw Holder Bottom.stl and clip.py.  The clip function will clip if 'Activate Clip' is true,
which can be set in the dialog or by changing the preferences file 'clip.csv' in the '.skeinforge' folder in your home directory
with a text editor or a spreadsheet program set to separate tabs.  The functions writeOutput and getClipChainGcode check
to see if the text has been clipped, if not they call getCombChainGcode in comb.py to comb the text; once they have the
combed text, then they clip.


> python clip.py
This brings up the dialog, after clicking 'Clip', the following is printed:
File Screw Holder Bottom.stl is being chain clipped.
The clipped file is saved as Screw Holder Bottom_clip.gcode


> python clip.py Screw Holder Bottom.stl
File Screw Holder Bottom.stl is being chain clipped.
The clipped file is saved as Screw Holder Bottom_clip.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 clip
>>> clip.main()
This brings up the clip dialog.


>>> clip.writeOutput()
Screw Holder Bottom.stl
File Screw Holder Bottom.stl is being chain clipped.
The clipped file is saved as Screw Holder Bottom_clip.gcode


>>> clip.getClipGcode("
( GCode generated by May 8, 2008 carve.py )
( Extruder Initialization )
..
many lines of gcode
..
")


>>> clip.getClipChainGcode("
( GCode generated by May 8, 2008 carve.py )
( Extruder Initialization )
..
many lines of 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.skeinforge_utilities import euclidean
from skeinforge_tools.skeinforge_utilities import gcodec
from skeinforge_tools.skeinforge_utilities import intercircle
from skeinforge_tools.skeinforge_utilities import preferences
from skeinforge_tools import analyze
from skeinforge_tools import comb
from skeinforge_tools.skeinforge_utilities import interpret
from skeinforge_tools import polyfile
import cStringIO
import math
import sys
import time


__author__ = "Enrique Perez (perez_enrique@yahoo.com)"
__date__ = "$Date: 2008/21/04 $"
__license__ = "GPL 3.0"

def getClipChainGcode( fileName, gcodeText, loopTailorPreferences = None ):
	"Clip a gcode linear move text.  Chain clip the gcode if it is not already clipped."
	gcodeText = gcodec.getGcodeFileText( fileName, gcodeText )
	if not gcodec.isProcedureDone( gcodeText, 'comb' ):
		gcodeText = comb.getCombChainGcode( fileName, gcodeText )
	return getClipGcode( gcodeText, loopTailorPreferences )

def getClipGcode( gcodeText, loopTailorPreferences = None ):
	"Clip a gcode linear move text."
	if gcodeText == '':
		return ''
	if gcodec.isProcedureDone( gcodeText, 'clip' ):
		return gcodeText
	if loopTailorPreferences == None:
		loopTailorPreferences = ClipPreferences()
		preferences.readPreferences( loopTailorPreferences )
	if not loopTailorPreferences.activateClip.value:
		return gcodeText
	skein = ClipSkein()
	skein.parseGcode( gcodeText, loopTailorPreferences )
	return skein.output.getvalue()

def writeOutput( fileName = '' ):
	"Clip a gcode linear move file.  Chain clip the gcode if it is not already clipped.  If no fileName is specified, clip the first unmodified gcode file in this folder."
	if fileName == '':
		unmodified = interpret.getGNUTranslatorFilesUnmodified()
		if len( unmodified ) == 0:
			print( "There are no unmodified gcode files in this folder." )
			return
		fileName = unmodified[ 0 ]
	loopTailorPreferences = ClipPreferences()
	preferences.readPreferences( loopTailorPreferences )
	startTime = time.time()
	print( 'File ' + gcodec.getSummarizedFilename( fileName ) + ' is being chain clipped.' )
	suffixFilename = fileName[ : fileName.rfind( '.' ) ] + '_clip.gcode'
	loopTailorGcode = getClipChainGcode( fileName, '', loopTailorPreferences )
	if loopTailorGcode == '':
		return
	gcodec.writeFileText( suffixFilename, loopTailorGcode )
	print( 'The clipped file is saved as ' + gcodec.getSummarizedFilename( suffixFilename ) )
	analyze.writeOutput( suffixFilename, loopTailorGcode )
	print( 'It took ' + str( int( round( time.time() - startTime ) ) ) + ' seconds to clip the file.' )


class ClipPreferences:
	"A class to handle the clip preferences."
	def __init__( self ):
		"Set the default preferences, execute title & preferences fileName."
		#Set the default preferences.
		self.archive = []
		self.activateClip = preferences.BooleanPreference().getFromValue( 'Activate Clip', True )
		self.archive.append( self.activateClip )
		self.clipOverExtrusionWidth = preferences.FloatPreference().getFromValue( 'Clip Over Extrusion Width (ratio):', 0.15 )
		self.archive.append( self.clipOverExtrusionWidth )
		self.fileNameInput = preferences.Filename().getFromFilename( interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File to be Clipped', '' )
		self.archive.append( self.fileNameInput )
		#Create the archive, title of the execute button, title of the dialog & preferences fileName.
		self.executeTitle = 'Clip'
		self.saveTitle = 'Save Preferences'
		preferences.setHelpPreferencesFileNameTitleWindowPosition( self, 'skeinforge_tools.clip.html' )

	def execute( self ):
		"Clip button has been clicked."
		fileNames = polyfile.getFileOrDirectoryTypesUnmodifiedGcode( self.fileNameInput.value, interpret.getImportPluginFilenames(), self.fileNameInput.wasCancelled )
		for fileName in fileNames:
			writeOutput( fileName )


class ClipSkein:
	"A class to clip a skein of extrusions."
	def __init__( self ):
		self.feedrateMinute = None
		self.isLoopPerimeter = False
		self.loopPath = None
		self.lineIndex = 0
		self.oldLocation = None
		self.output = cStringIO.StringIO()

	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.addLine( 'M101' )
		for point in thread[ 1 : ]:
			self.addGcodeMovementZ( self.feedrateMinute, point, z )

	def addGcodeMovementZ( self, feedrateMinute, point, z ):
		"Add a movement to the output."
		self.addLine( 'G1 X%s Y%s Z%s F%s' % ( self.getRounded( point.real ), self.getRounded( point.imag ), self.getRounded( z ), self.getRounded( feedrateMinute ) ) )

	def addLine( self, line ):
		"Add a line of text and a newline to the output."
		self.output.write( line + "\n" )

	def addTailoredLoopPath( self ):
		"Add a clipped and jittered loop path."
		if self.clipLength > 0.0:
			self.loopPath.path = euclidean.getClippedLoopPath( self.clipLength, self.loopPath.path )
			self.loopPath.path = euclidean.getSimplifiedPath( self.loopPath.path, self.extrusionWidth )
		self.addGcodeFromThreadZ( self.loopPath.path, self.loopPath.z )
		self.loopPath = None

	def getRounded( self, number ):
		"Get number rounded to the number of carried decimal places as a string."
		return euclidean.getRoundedToDecimalPlacesString( self.decimalPlacesCarried, number )

	def isNextExtruderOn( self ):
		"Determine if there is an extruder on command before a move command."
		line = self.lines[ self.lineIndex ]
		splitLine = line.split()
		for afterIndex in xrange( self.lineIndex + 1, len( self.lines ) ):
			line = self.lines[ afterIndex ]
			splitLine = line.split()
			firstWord = gcodec.getFirstWord( splitLine )
			if firstWord == 'G1' or firstWord == 'M103':
				return False
			elif firstWord == 'M101':
				return True
		return False

	def linearMove( self, 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.loopPath != None:
			self.loopPath.path.append( location.dropAxis( 2 ) )
		self.oldLocation = location

	def parseGcode( self, gcodeText, loopTailorPreferences ):
		"Parse gcode text and store the clip gcode."
		self.lines = gcodec.getTextLines( gcodeText )
		self.parseInitialization( loopTailorPreferences )
		for self.lineIndex in xrange( self.lineIndex, len( self.lines ) ):
			line = self.lines[ self.lineIndex ]
			self.parseLine( line )

	def parseInitialization( self, loopTailorPreferences ):
		"Parse gcode initialization and store the parameters."
		for self.lineIndex in xrange( len( self.lines ) ):
			line = self.lines[ self.lineIndex ]
			splitLine = line.split()
			firstWord = gcodec.getFirstWord( splitLine )
			if firstWord == '(<extrusionWidth>':
				self.extrusionWidth = float( splitLine[ 1 ] )
				self.clipLength = loopTailorPreferences.clipOverExtrusionWidth.value * self.extrusionWidth
			elif firstWord == '(<decimalPlacesCarried>':
				self.decimalPlacesCarried = int( splitLine[ 1 ] )
			elif firstWord == '(</extruderInitialization>)':
				self.addLine( '(<procedureDone> clip </procedureDone>)' )
				return
			elif firstWord == '(<travelFeedratePerSecond>':
				self.travelFeedratePerMinute = 60.0 * float( splitLine[ 1 ] )
			self.addLine( line )

	def parseLine( self, line ):
		"Parse a gcode line and add it to the clip skein."
		splitLine = line.split()
		if len( splitLine ) < 1:
			return
		firstWord = splitLine[ 0 ]
		if firstWord == 'G1':
			self.linearMove( splitLine )
		elif firstWord == 'M103':
			self.isLoopPerimeter = False
			if self.loopPath != None:
				self.addTailoredLoopPath()
		if firstWord == '(<loop>)' or firstWord == '(<perimeter>)':
			self.isLoopPerimeter = True
		if self.loopPath == None:
			self.addLine( line )


def main( hashtable = None ):
	"Display the clip dialog."
	if len( sys.argv ) > 1:
		writeOutput( ' '.join( sys.argv[ 1 : ] ) )
	else:
		preferences.displayDialog( ClipPreferences() )

if __name__ == "__main__":
	main()
