"""
Hop is a script to raise the extruder when it is not extruding.

The default 'Activate Hop' checkbox is off.  It is off because Vik and Nophead found better results without hopping.  When it
is on, the functions described below will work, when it is off, the functions will not be called.

The important value for the hop preferences is "Hop Over Layer Thickness (ratio)" which is the ratio of the hop height over the
layer thickness, the default is 1.0.  The 'Minimum Hop Angle (degrees)' is the minimum angle that the path of the extruder
will be raised.  An angle of ninety means that the extruder will go straight up as soon as it is not extruding and a low angle
means the extruder path will gradually rise to the hop height, the default is 20 degrees.

To run hop, in a shell which hop is in type:
> python hop.py

The following examples hop 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 hop.py.  The hop function will hop if the 'Activate Hop'
checkbox is on.  The functions writeOutput and getHopChainGcode check to see if the text has been hopped, if not they
call the getStretchChainGcode in stretch.py to stretch the text; once they have the stretched text, then they hop.


> python hop.py
This brings up the dialog, after clicking 'Hop', the following is printed:
File Screw Holder Bottom.stl is being chain hopped.
The hopped file is saved as Screw Holder Bottom_hop.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 hop
>>> hop.main()
This brings up the hop dialog.


>>> hop.writeOutput()
Screw Holder Bottom.stl
File Screw Holder Bottom.stl is being chain hopped.
The hopped file is saved as Screw Holder Bottom_hop.gcode


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


>>> hop.getHopChainGcode("
( 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 preferences
from skeinforge_tools import analyze
from skeinforge_tools.skeinforge_utilities import interpret
from skeinforge_tools import polyfile
from skeinforge_tools import stretch
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 getHopChainGcode( fileName, gcodeText, hopPreferences = None ):
	"Hop a gcode linear move text.  Chain hop the gcode if it is not already hopped."
	gcodeText = gcodec.getGcodeFileText( fileName, gcodeText )
	if not gcodec.isProcedureDone( gcodeText, 'stretch' ):
		gcodeText = stretch.getStretchChainGcode( fileName, gcodeText )
	return getHopGcode( gcodeText, hopPreferences )

def getHopGcode( gcodeText, hopPreferences = None ):
	"Hop a gcode linear move text."
	if gcodeText == '':
		return ''
	if gcodec.isProcedureDone( gcodeText, 'hop' ):
		return gcodeText
	if hopPreferences == None:
		hopPreferences = HopPreferences()
		preferences.readPreferences( hopPreferences )
	if not hopPreferences.activateHop.value:
		return gcodeText
	skein = HopSkein()
	skein.parseGcode( gcodeText, hopPreferences )
	return skein.output.getvalue()

def writeOutput( fileName = '' ):
	"Hop a gcode linear move file.  Chain hop the gcode if it is not already hopped. If no fileName is specified, hop 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 ]
	hopPreferences = HopPreferences()
	preferences.readPreferences( hopPreferences )
	startTime = time.time()
	print( 'File ' + gcodec.getSummarizedFilename( fileName ) + ' is being chain hopped.' )
	suffixFilename = fileName[ : fileName.rfind( '.' ) ] + '_hop.gcode'
	hopGcode = getHopChainGcode( fileName, '', hopPreferences )
	if hopGcode == '':
		return
	gcodec.writeFileText( suffixFilename, hopGcode )
	print( 'The hopped file is saved as ' + gcodec.getSummarizedFilename( suffixFilename ) )
	analyze.writeOutput( suffixFilename, hopGcode )
	print( 'It took ' + str( int( round( time.time() - startTime ) ) ) + ' seconds to hop the file.' )


class HopPreferences:
	"A class to handle the hop preferences."
	def __init__( self ):
		"Set the default preferences, execute title & preferences fileName."
		#Set the default preferences.
		self.archive = []
		self.activateHop = preferences.BooleanPreference().getFromValue( 'Activate Hop', False )
		self.archive.append( self.activateHop )
		self.fileNameInput = preferences.Filename().getFromFilename( interpret.getGNUTranslatorGcodeFileTypeTuples(), 'Open File to be Hopped', '' )
		self.archive.append( self.fileNameInput )
		self.hopOverLayerThickness = preferences.FloatPreference().getFromValue( 'Hop Over Layer Thickness (ratio):', 1.0 )
		self.archive.append( self.hopOverLayerThickness )
		self.minimumHopAngle = preferences.FloatPreference().getFromValue( 'Minimum Hop Angle (degrees):', 30.0 )
		self.archive.append( self.minimumHopAngle )
		#Create the archive, title of the execute button, title of the dialog & preferences fileName.
		self.executeTitle = 'Hop'
		self.saveTitle = 'Save Preferences'
		preferences.setHelpPreferencesFileNameTitleWindowPosition( self, 'skeinforge_tools.hop.html' )

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


class HopSkein:
	"A class to hop a skein of extrusions."
	def __init__( self ):
		self.decimalPlacesCarried = 3
		self.extruderActive = False
		self.feedrateString = ''
		self.hopHeight = 0.4
		self.hopDistance = self.hopHeight
		self.justDeactivated = False
		self.lineIndex = 0
		self.lines = None
		self.oldLocation = None
		self.output = cStringIO.StringIO()

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

	def getHopLine( self, line ):
		"Get hopped gcode line."
		splitLine = line.split( ' ' )
		indexOfF = gcodec.indexOfStartingWithSecond( "F", splitLine )
		if indexOfF > 0:
			self.feedrateString = splitLine[ indexOfF ]
		if self.extruderActive:
			return line
		location = gcodec.getLocationFromSplitLine( self.oldLocation, splitLine )
		highestZ = location.z
		if self.oldLocation != None:
			highestZ = max( highestZ, self.oldLocation.z )
		locationComplex = location.dropAxis( 2 )
		if self.justDeactivated:
			oldLocationComplex = self.oldLocation.dropAxis( 2 )
			distance = abs( locationComplex - oldLocationComplex )
			if distance < self.minimumDistance:
				if self.isNextTravel():
					return self.getMovementLineWithHop( locationComplex, highestZ )
			alongRatio = min( 0.41666666, self.hopDistance / distance )
			oneMinusAlong = 1.0 - alongRatio
			closeLocation = oldLocationComplex * oneMinusAlong + locationComplex * alongRatio
			self.addLine( self.getMovementLineWithHop( locationComplex, highestZ ) )
			if self.isNextTravel():
				return self.getMovementLineWithHop( locationComplex, highestZ )
			farLocation = oldLocationComplex * alongRatio + locationComplex * oneMinusAlong
			self.addLine( self.getMovementLineWithHop( farLocation, highestZ ) )
			return line
		if self.isNextTravel():
			return self.getMovementLineWithHop( locationComplex, highestZ )
		return line

	def getMovementLineWithHop( self, location, z ):
		"Get linear movement line for a location."
		movementLine = 'G1 X%s Y%s Z%s' % ( self.getRounded( location.real ), self.getRounded( location.imag ), self.getRounded( z + self.hopHeight ) )
		if self.feedrateString != '':
			movementLine += ' ' + self.feedrateString
		return movementLine

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

	def isNextTravel( self ):
		"Determine if there is another linear travel before the thread ends."
		for afterIndex in xrange( self.lineIndex + 1, len( self.lines ) ):
			line = self.lines[ afterIndex ]
			splitLine = line.split( ' ' )
			firstWord = "";
			if len( splitLine ) > 0:
				firstWord = splitLine[ 0 ]
			if firstWord == 'G1':
				return True
			if firstWord == 'M101':
				return False
		return False

	def parseGcode( self, gcodeText, hopPreferences ):
		"Parse gcode text and store the hop gcode."
		self.lines = gcodec.getTextLines( gcodeText )
		self.minimumSlope = math.tan( math.radians( hopPreferences.minimumHopAngle.value ) )
		self.parseInitialization( hopPreferences )
		for self.lineIndex in xrange( self.lineIndex, len( self.lines ) ):
			line = self.lines[ self.lineIndex ]
			self.parseLine( line )

	def parseInitialization( self, hopPreferences ):
		"Parse gcode initialization and store the parameters."
		for self.lineIndex in xrange( len( self.lines ) ):
			line = self.lines[ self.lineIndex ]
			splitLine = line.split()
			firstWord = ''
			if len( splitLine ) > 0:
				firstWord = splitLine[ 0 ]
			if firstWord == '(<layerThickness>':
				layerThickness = float( splitLine[ 1 ] )
				self.hopHeight = hopPreferences.hopOverLayerThickness.value * layerThickness
				self.hopDistance = self.hopHeight / self.minimumSlope
				self.minimumDistance = 0.5 * layerThickness
			elif firstWord == '(<decimalPlacesCarried>':
				self.decimalPlacesCarried = int( splitLine[ 1 ] )
			elif firstWord == '(</extruderInitialization>)':
				self.addLine( '(<procedureDone> hop </procedureDone>)' )
				return
			self.addLine( line )

	def parseLine( self, line ):
		"Parse a gcode line and add it to the bevel gcode."
		splitLine = line.split()
		if len( splitLine ) < 1:
			return
		firstWord = splitLine[ 0 ]
		if firstWord == 'G1':
			line = self.getHopLine( line )
			self.oldLocation = gcodec.getLocationFromSplitLine( self.oldLocation, splitLine )
			self.justDeactivated = False
		elif firstWord == 'M101':
			self.extruderActive = True
		elif firstWord == 'M103':
			self.extruderActive = False
			self.justDeactivated = True
		self.addLine( line )


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

if __name__ == "__main__":
	main()
