# coding=utf-8
from __future__ import absolute_import
__author__ = "Florian Becker <florian@mr-beam.org> based on work by Gina Häußge and David Braam"
__license__ = "GNU Affero General Public License http://www.gnu.org/licenses/agpl.html"
__copyright__ = "Copyright (C) 2013 David Braam - Released under terms of the AGPLv3 License"

import os
import threading
import logging
import glob
import time
import serial
import re
import Queue

from yaml import load as yamlload
from yaml import dump as yamldump
from subprocess import call as subprocesscall

import octoprint.plugin

from octoprint.settings import settings, default_settings
from octoprint.events import eventManager, Events
from octoprint.filemanager.destinations import FileDestinations
from octoprint.util import get_exception_string, RepeatedTimer, CountedEvent, sanitize_ascii

### MachineCom #########################################################################################################
class MachineCom(object):
	STATE_NONE = 0
	STATE_OPEN_SERIAL = 1
	STATE_DETECT_SERIAL = 2
	STATE_CONNECTING = 3
	STATE_OPERATIONAL = 4
	STATE_PRINTING = 5
	STATE_PAUSED = 6
	STATE_CLOSED = 7
	STATE_ERROR = 8
	STATE_CLOSED_WITH_ERROR = 9
	STATE_LOCKED = 10
	STATE_HOMING = 11
	STATE_FLASHING = 12

	def __init__(self, port=None, baudrate=None, callbackObject=None, printerProfileManager=None):
		self._logger = logging.getLogger(__name__)
		self._serialLogger = logging.getLogger("SERIAL")

		if port is None:
			port = settings().get(["serial", "port"])
		elif isinstance(port, list):
			port = port[0]
		if baudrate is None:
			settingsBaudrate = settings().getInt(["serial", "baudrate"])
			if settingsBaudrate is None:
				baudrate = 0
			else:
				baudrate = settingsBaudrate
		if callbackObject is None:
			callbackObject = MachineComPrintCallback()

		self._port = port
		self._baudrate = baudrate
		self._callback = callbackObject
		self._printerProfileManager = printerProfileManager

		self.RX_BUFFER_SIZE = 127

		self._state = self.STATE_NONE
		self._grbl_state = None
		self._errorValue = "Unknown Error"
		self._serial = None
		self._currentFile = None
		self._status_timer = None
		self._acc_line_buffer = []
		self._cmd = None
		self._pauseWaitStartTime = None
		self._pauseWaitTimeLost = 0.0
		self._commandQueue = Queue.Queue()
		self._send_event = CountedEvent(max=50)
		self._finished_currentFile = False
		self._pause_delay_time = 0
		self._feedrate_factor = 1
		self._actual_feedrate = None
		self._intensity_factor = 1
		self._actual_intensity = None
		self._feedrate_dict = {}
		self._intensity_dict = {}
		self._passes = 1
		self._finished_passes = 0

		# regular expressions
		self._regex_command = re.compile("^\s*\$?([GM]\d+|[THX])")
		self._regex_feedrate = re.compile("F\d+", re.IGNORECASE)
		self._regex_intensity = re.compile("S\d+", re.IGNORECASE)

		self._real_time_commands={'poll_status':False,
								'feed_hold':False,
								'cycle_start':False,
								'soft_reset':False}

		# metric stuff
		#self._metric_chars = 0
		#self._metric_time = None
		#self._metric_active = True
		#self._metric_thread = threading.Thread(target=self._metric_loop, name="comm._metric_thread")
		#self._metric_thread.daemon = True
		#self._metric_thread.start()

		# hooks
		self._pluginManager = octoprint.plugin.plugin_manager()
		self._serial_factory_hooks = self._pluginManager.get_hooks("octoprint.comm.transport.serial.factory")

		# monitoring thread
		self._monitoring_active = True
		self.monitoring_thread = threading.Thread(target=self._monitor_loop, name="comm._monitoring_thread")
		self.monitoring_thread.daemon = True
		self.monitoring_thread.start()

		# sending thread
		self._sending_active = True
		self.sending_thread = threading.Thread(target=self._send_loop, name="comm.sending_thread")
		self.sending_thread.daemon = True

	def _monitor_loop(self):
		#Open the serial port.
		if not self._openSerial():
			return

		self._log("Connected to: %s, starting monitor" % self._serial)
		self._changeState(self.STATE_CONNECTING)
		self._timeout = get_new_timeout("communication")

		while self._monitoring_active:
			try:
				line = self._readline()
				if line is None:
					break
				if line.strip() is not "":
					self._timeout = get_new_timeout("communication")
				if line.startswith('<'): # status report
					self._handle_status_report(line)
				elif line.startswith('ok'): # ok message :)
					self._handle_ok_message()
				elif line.startswith('err'): # error message
					self._handle_error_message(line)
				elif line.startswith('ALA'): # ALARM message
					self._handle_alarm_message(line)
				elif line.startswith('['): # feedback message
					self._handle_feedback_message(line)
				elif line.startswith('Grb'): # Grbl startup message
					self._handle_startup_message(line)
			except:
				self._logger.exception("Something crashed inside the monitoring loop, please report this to Mr. Beam")
				errorMsg = "See octoprint.log for details"
				self._log(errorMsg)
				self._errorValue = errorMsg
				self._changeState(self.STATE_ERROR)
				eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
		self._log("Connection closed, closing down monitor")

	def _send_loop(self):
		while self._sending_active:
			try:
				self._process_rt_commands()
				if self.isPrinting() and self._commandQueue.empty():
					cmd = self._getNext()
					if cmd is not None:
						self.sendCommand(cmd)
						self._callback.on_comm_progress()
					else:
						if self._finished_passes >= self._passes:
							if len(self._acc_line_buffer) == 0:
								self._set_print_finished()
						self._currentFile.resetToBeginning()
						cmd = self._getNext()
						if cmd is not None:
							self.sendCommand(cmd)
							self._callback.on_comm_progress()

				self._sendCommand()
				self._send_event.wait(1)
				self._send_event.clear()
			except:
				self._logger.exception("Something crashed inside the sending loop, please report this to Mr. Beam")
				errorMsg = "See octoprint.log for details"
				self._log(errorMsg)
				self._errorValue = errorMsg
				self._changeState(self.STATE_ERROR)
				eventManager().fire(Events.ERROR, {"error": self.getErrorString()})

	def _metric_loop(self):
		self._metricf = open('metric.tmp','w')
		self._metricf.write("1 sec interval")
		while self._metric_active:
			time.sleep(1)
			if self._metric_time is not None:
				t = time.time()
				#s = "Metric: %f [chars/sec]" % (self._metric_chars / (t - self._metric_time))
				s = "%.2f" % (self._metric_chars / (t - self._metric_time))
				self._metric_time = None
				self._metric_chars = 0
				self._metricf.write(s)
				#self._log(s)
		self._metricf.close()

	def _sendCommand(self, cmd=None):
		if cmd is None:
			if self._cmd is None and self._commandQueue.empty():
				return
			elif self._cmd is None:
				self._cmd = self._commandQueue.get()
			if sum([len(x) for x in self._acc_line_buffer]) + len(self._cmd) +1 < self.RX_BUFFER_SIZE-5:
				self._cmd, _, _  = self._process_command_phase("sending", self._cmd)
				self._log("Send: %s" % self._cmd)
				self._acc_line_buffer.append(self._cmd + '\n')
				try:
					self._serial.write(self._cmd + '\n')
					self._process_command_phase("sent", self._cmd)
					self._cmd = None
					self._send_event.set()
				except serial.SerialException:
					self._log("Unexpected error while writing serial port: %s" % (get_exception_string()))
					self._errorValue = get_exception_string()
					self.close(True)
		else:
			cmd, _, _  = self._process_command_phase("sending", cmd)
			self._log("Send: %s" % cmd)
			try:

				self._serial.write(cmd)
				self._process_command_phase("sent", cmd)
			except serial.SerialException:
				self._log("Unexpected error while writing serial port: %s" % (get_exception_string()))
				self._errorValue = get_exception_string()
				self.close(True)

	def _process_rt_commands(self):
		if self._real_time_commands['poll_status']:
			self._sendCommand('?')
			self._real_time_commands['poll_status']=False
		elif self._real_time_commands['feed_hold']:
			self._sendCommand('!')
			self._real_time_commands['feed_hold']=False
		elif self._real_time_commands['cycle_start']:
			self._sendCommand('~')
			self._real_time_commands['cycle_start']=False
		elif self._real_time_commands['soft_reset']:
			self._sendCommand(b'\x18')
			self._real_time_commands['soft_reset']=False

	def _openSerial(self):
		def default(_, port, baudrate, read_timeout):
			if port is None or port == 'AUTO':
				# no known port, try auto detection
				self._changeState(self.STATE_DETECT_SERIAL)
				ser = self._detectPort(True)
				if ser is None:
					self._errorValue = 'Failed to autodetect serial port, please set it manually.'
					self._changeState(self.STATE_ERROR)
					eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
					self._log("Failed to autodetect serial port, please set it manually.")
					return None
				port = ser.port

			# connect to regular serial port
			self._log("Connecting to: %s" % port)
			if baudrate == 0:
				baudrates = baudrateList()
				ser = serial.Serial(str(port), 115200 if 115200 in baudrates else baudrates[0], timeout=read_timeout, writeTimeout=10000, parity=serial.PARITY_ODD)
			else:
				ser = serial.Serial(str(port), baudrate, timeout=read_timeout, writeTimeout=10000, parity=serial.PARITY_ODD)
			ser.close()
			ser.parity = serial.PARITY_NONE
			ser.open()
			return ser

		serial_factories = self._serial_factory_hooks.items() + [("default", default)]
		for name, factory in serial_factories:
			try:
				serial_obj = factory(self, self._port, self._baudrate, settings().getFloat(["serial", "timeout", "connection"]))
			except (OSError, serial.SerialException):
				exception_string = get_exception_string()
				self._errorValue = "Connection error, see Terminal tab"
				self._changeState(self.STATE_ERROR)
				eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
				self._log("Unexpected error while connecting to serial port: %s %s (hook %s)" % (self._port, exception_string, name))
				if "failed to set custom baud rate" in exception_string.lower():
					self._log("Your installation does not support custom baudrates (e.g. 250000) for connecting to your printer. This is a problem of the pyserial library that OctoPrint depends on. Please update to a pyserial version that supports your baudrate or switch your printer's firmware to a standard baudrate (e.g. 115200). See https://github.com/foosel/OctoPrint/wiki/OctoPrint-support-for-250000-baud-rate-on-Raspbian")
				return False
			if serial_obj is not None:
				# first hook to succeed wins, but any can pass on to the next
				self._log(repr(self._serial))
				self._changeState(self.STATE_OPEN_SERIAL)
				self._serial = serial_obj
				return True
		return False

	def _readline(self):
		if self._serial is None:
			return None
		try:
			ret = self._serial.readline()
			self._send_event.set()
			if('ok' in ret or 'error' in ret):
				if(len(self._acc_line_buffer) > 0):
					del self._acc_line_buffer[0]  # Delete the commands character count corresponding to the last 'ok'
		except serial.SerialException:
			self._log("Unexpected error while reading serial port: %s" % (get_exception_string()))
			self._errorValue = get_exception_string()
			self.close(True)
			return None
		if ret == '': return ''
		try:
			self._log("Recv: %s" % sanitize_ascii(ret))
		except ValueError as e:
			self._log("WARN: While reading last line: %s" % e)
			self._log("Recv: %r" % ret)
		return ret

	def _getNext(self):
		if self._finished_currentFile is False:
			line = self._currentFile.getNext()
			if line is None:
				self._finished_passes += 1
				if self._finished_passes >= self._passes:
					self._finished_currentFile = True
			return line
		else:
			return None

	def _set_print_finished(self):
		self._callback.on_comm_print_job_done()
		self._changeState(self.STATE_OPERATIONAL)
		payload = {
			"file": self._currentFile.getFilename(),
			"filename": os.path.basename(self._currentFile.getFilename()),
			"origin": self._currentFile.getFileLocation(),
			"time": self.getPrintTime()
		}
		eventManager().fire(Events.PRINT_DONE, payload)
		self.sendCommand("M5")
		self.sendCommand("G0X0Y0")
		self.sendCommand("M9")

	def _handle_status_report(self, line):
		self._grbl_state = line[1:].split(',')[0]
		if self._grbl_state == 'Queue':
			if time.time() - self._pause_delay_time > 0.3:
				if not self.isPaused():
					self.setPause(True, False)
		elif self._grbl_state == 'Run' or self._grbl_state == 'Idle':
			if time.time() - self._pause_delay_time > 0.3:
				if self.isPaused():
					self.setPause(False, False)
		self._update_grbl_pos(line)
		#if self._metricf is not None:
		#	self._metricf.write(line)

	def _handle_ok_message(self):
		if self._state == self.STATE_HOMING:
			self._changeState(self.STATE_OPERATIONAL)

	def _handle_error_message(self, line):
		self._errorValue = line
		eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
		self._changeState(self.STATE_LOCKED)

	def _handle_alarm_message(self, line):
		if "Hard/soft limit" in line:
			errorMsg = "Machine Limit Hit. Please reset the machine and do a homing cycle"
			self._log(errorMsg)
			self._errorValue = errorMsg
			eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
			eventManager().fire(Events.LIMITS_HIT, {"error": self.getErrorString()})
		elif "Abort during cycle" in line:
			errorMsg = "Soft-reset detected. Please do a homing cycle"
			self._log(errorMsg)
			self._errorValue = errorMsg
			eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
			eventManager().fire(Events.SOFT_RESET, {"error": self.getErrorString()})
		elif "Probe fail" in line:
			errorMsg = "Probing has failed. Please reset the machine and do a homing cycle"
			self._log(errorMsg)
			self._errorValue = errorMsg
			eventManager().fire(Events.ERROR, {"error": self.getErrorString()})

		with self._commandQueue.mutex:
			self._commandQueue.queue.clear()
		self._acc_line_buffer = []
		self._send_event.clear(completely=True)
		self._changeState(self.STATE_LOCKED)

		# close and open serial port to reset arduino
		self._serial.close()
		self._openSerial()

	def _handle_feedback_message(self, line):
		if line[1:].startswith('Res'): # [Reset to continue]
			pass
		elif line[1:].startswith('\'$H'): # ['$H'|'$X' to unlock]
			pass
		elif line[1:].startswith('Cau'): # [Caution: Unlocked]
			pass
		elif line[1:].startswith('Ena'): # [Enabled]
			pass
		elif line[1:].startswith('Dis'): # [Disabled]
			pass

	def _handle_startup_message(self, line):
		if self.isOperational():
			errorMsg = "Machine reset."
			self._cmd = None
			self._acc_line_buffer = []
			self._pauseWaitStartTime = None
			self._pauseWaitTimeLost = 0.0
			self._send_event.clear(completely=True)
			with self._commandQueue.mutex:
				self._commandQueue.queue.clear()
			self._log(errorMsg)
			self._errorValue = errorMsg
			self._changeState(self.STATE_LOCKED)
			eventManager().fire(Events.ERROR, {"error": self.getErrorString()})
		else:
			versionMatch = re.search("Grbl (?P<grbl>.+?)(_(?P<git>[0-9a-f]{7})(?P<dirty>-dirty)?)? \[.+\]", line)
			if versionMatch:
				versionDict = versionMatch.groupdict()
				self._writeGrblVersionToFile(versionDict)
				if self._compareGrblVersion(versionDict) is False:
					self._flashGrbl()
				else:
					self._onConnected(self.STATE_LOCKED)

	def _update_grbl_pos(self, line):
		# line example:
		# <Idle,MPos:-434.000,-596.000,0.000,WPos:0.000,0.000,0.000,S:0,laser off:0>
		try:
			idx_mx_begin = line.index('MPos:') + 5
			idx_mx_end = line.index('.', idx_mx_begin) + 2
			idx_my_begin = line.index(',', idx_mx_end) + 1
			idx_my_end = line.index('.', idx_my_begin) + 2
			#idx_mz_begin = line.index(',', idx_my_end) + 1
			#idx_mz_end = line.index('.', idx_mz_begin) + 2

			idx_wx_begin = line.index('WPos:') + 5
			idx_wx_end = line.index('.', idx_wx_begin) + 2
			idx_wy_begin = line.index(',', idx_wx_end) + 1
			idx_wy_end = line.index('.', idx_wy_begin) + 2
			#idx_wz_begin = line.index(',', idx_wy_end) + 1
			#idx_wz_end = line.index('.', idx_wz_begin) + 2

			#idx_intensity_begin = line.index('S:', idx_wz_end) + 2
			#idx_intensity_end = line.index(',', idx_intensity_begin)

			#idx_laserstate_begin = line.index('laser ', idx_intensity_end) + 6
			#idx_laserstate_end = line.index(':', idx_laserstate_begin)

			#payload = {
			#"mx": line[idx_mx_begin:idx_mx_end],
			#"my": line[idx_my_begin:idx_my_end],
			#"mz": line[idx_mz_begin:idx_mz_end],
			#"wx": line[idx_wx_begin:idx_wx_end],
			#"wy": line[idx_wy_begin:idx_wy_end],
			#"wz": line[idx_wz_begin:idx_wz_end],
			#"laser": line[idx_laserstate_begin:idx_laserstate_end],
			#"intensity": line[idx_intensity_begin:idx_intensity_end]
			#}
			mx = float(line[idx_mx_begin:idx_mx_end])
			my = float(line[idx_my_begin:idx_my_end])
			wx = float(line[idx_wx_begin:idx_wx_end])
			wy = float(line[idx_wy_begin:idx_wy_end])
			self._callback.on_comm_pos_update([mx, my, 0], [wx, wy, 0])
		#eventManager().fire(Events.RT_STATE, payload)
		except ValueError:
			pass

	def _process_command_phase(self, phase, command, command_type=None, gcode=None):
		if phase not in ("queuing", "queued", "sending", "sent"):
			return command, command_type, gcode

		if gcode is None:
			gcode = self._gcode_command_for_cmd(command)

		# if it's a gcode command send it through the specific handler if it exists
		if gcode is not None:
			gcodeHandler = "_gcode_" + gcode + "_" + phase
			if hasattr(self, gcodeHandler):
				handler_result = getattr(self, gcodeHandler)(command, cmd_type=command_type)
				command, command_type, gcode = self._handle_command_handler_result(command, command_type, gcode, handler_result)

		# finally return whatever we resulted on
		return command, command_type, gcode

	def _gcode_command_for_cmd(self, cmd):
		"""
		Tries to parse the provided ``cmd`` and extract the GCODE command identifier from it (e.g. "G0" for "G0 X10.0").

		Arguments:
		    cmd (str): The command to try to parse.

		Returns:
		    str or None: The GCODE command identifier if it could be parsed, or None if not.
		"""
		if not cmd:
			return None

		if cmd == '!': return 'Hold'
		if cmd == '~': return 'Resume'

		gcode = self._regex_command.search(cmd)
		if not gcode:
			return None

		return gcode.group(1)

	# internal state management
	def _changeState(self, newState):
		if self._state == newState:
			return

		if newState == self.STATE_PRINTING:
			if self._status_timer is not None:
				self._status_timer.cancel()
			self._status_timer = RepeatedTimer(1, self._poll_status)
			self._status_timer.start()
		elif newState == self.STATE_OPERATIONAL:
			if self._status_timer is not None:
				self._status_timer.cancel()
			self._status_timer = RepeatedTimer(2, self._poll_status)
			self._status_timer.start()
		elif newState == self.STATE_PAUSED:
			if self._status_timer is not None:
				self._status_timer.cancel()
			self._status_timer = RepeatedTimer(0.2, self._poll_status)
			self._status_timer.start()

		if newState == self.STATE_CLOSED or newState == self.STATE_CLOSED_WITH_ERROR:
			if self._currentFile is not None:
				self._currentFile.close()
			self._log("entered state closed / closed with error. reseting character counter.")
			self.acc_line_lengths = []

		oldState = self.getStateString()
		self._state = newState
		self._log('Changing monitoring state from \'%s\' to \'%s\'' % (oldState, self.getStateString()))
		self._callback.on_comm_state_change(newState)

	def _onConnected(self, nextState):
		self._serial.timeout = settings().getFloat(["serial", "timeout", "communication"])

		if(nextState is None):
			self._changeState(self.STATE_LOCKED)
		else:
			self._changeState(nextState)

		if not self.sending_thread.isAlive():
			self.sending_thread.start()

		payload = dict(port=self._port, baudrate=self._baudrate)
		eventManager().fire(Events.CONNECTED, payload)

	def _detectPort(self, close):
		self._log("Serial port list: %s" % (str(serialList())))
		for p in serialList():
			try:
				self._log("Connecting to: %s" % (p))
				serial_obj = serial.Serial(p)
				if close:
					serial_obj.close()
				return serial_obj
			except (OSError, serial.SerialException) as e:
				self._log("Error while connecting to %s: %s" % (p, str(e)))
		return None

	def _poll_status(self):
		if self.isOperational():
			self._real_time_commands['poll_status']=True
			self._send_event.set()

	def _soft_reset(self):
		if self.isOperational():
			self._real_time_commands['soft_reset']=True
			self._send_event.set()

	def _log(self, message):
		self._callback.on_comm_log(message)
		self._serialLogger.debug(message)

	def _compareGrblVersion(self, versionDict):
		cwd = os.path.dirname(__file__)
		with open(cwd + "/../grbl/grblVersionRequirement.yml", 'r') as infile:
			grblReqDict = yamlload(infile)
		requiredGrblVer = str(grblReqDict['grbl']) + '_' + str(grblReqDict['git'])
		if grblReqDict['dirty'] is True:
			requiredGrblVer += '-dirty'
		actualGrblVer = str(versionDict['grbl']) + '_' + str(versionDict['git'])
		if versionDict['dirty'] is not(None):
			actualGrblVer += '-dirty'
		# compare actual and required grbl version
		self._requiredGrblVer = requiredGrblVer
		self._actualGrblVer = actualGrblVer
		print repr(requiredGrblVer)
		print repr(actualGrblVer)
		if requiredGrblVer != actualGrblVer:
			self._log("unsupported grbl version detected...")
			self._log("required: " + requiredGrblVer)
			self._log("detected: " + actualGrblVer)
			return False
		else:
			return True

	def _flashGrbl(self):
		self._changeState(self.STATE_FLASHING)
		self._serial.close()
		cwd = os.path.dirname(__file__)
		pathToGrblHex = cwd + "/../grbl/grbl.hex"

		# TODO check if avrdude is installed.
		# TODO log in logfile as well, not only to the serial monitor (use self._logger.info()... )
		params = ["avrdude", "-patmega328p", "-carduino", "-b" + str(self._baudrate), "-P" + str(self._port), "-D", "-Uflash:w:" + pathToGrblHex]
		rc = subprocesscall(params)

		if rc == 0:
			self._log("successfully flashed new grbl version")
			self._openSerial()
			self._changeState(self.STATE_CONNECTING)
		else:
			self._log("error during flashing of new grbl version")
			self._errorValue = "avrdude returncode: %s" % rc
			self._changeState(self.STATE_CLOSED_WITH_ERROR)

	@staticmethod
	def _writeGrblVersionToFile(versionDict):
		if versionDict['dirty'] == '-dirty':
			versionDict['dirty'] = True
		versionDict['lastConnect'] = time.time()
		versionFile = os.path.join(settings().getBaseFolder("logs"), 'grbl_Version.yml')
		with open(versionFile, 'w') as outfile:
			outfile.write(yamldump(versionDict, default_flow_style=True))

	def _handle_command_handler_result(self, command, command_type, gcode, handler_result):
		original_tuple = (command, command_type, gcode)

		if handler_result is None:
			# handler didn't return anything, we'll just continue
			return original_tuple

		if isinstance(handler_result, basestring):
			# handler did return just a string, we'll turn that into a 1-tuple now
			handler_result = (handler_result,)
		elif not isinstance(handler_result, (tuple, list)):
			# handler didn't return an expected result format, we'll just ignore it and continue
			return original_tuple

		hook_result_length = len(handler_result)
		if hook_result_length == 1:
			# handler returned just the command
			command, = handler_result
		elif hook_result_length == 2:
			# handler returned command and command_type
			command, command_type = handler_result
		else:
			# handler returned a tuple of an unexpected length
			return original_tuple

		gcode = self._gcode_command_for_cmd(command)
		return command, command_type, gcode

	def _set_feedrate_override(self, value):
		temp = value / 100.0
		if temp > 0:
			self._feedrate_factor = temp
			self._feedrate_dict = {}
			if self._actual_feedrate is not None:
				temp = round(self._actual_feedrate * self._feedrate_factor)
				# TODO replace with value from printer profile
				if temp > 5000:
					temp = 5000
				self.sendCommand('F%d' % round(temp))

	def _set_intensity_override(self, value):
		temp = value / 100.0
		if temp >= 0:
			self._intensity_factor = temp
			self._intensity_dict = {}
			if self._actual_intensity is not None:
				temp = round(self._actual_intensity * self._intensity_factor)
				if temp > 1000:
					temp = 1000
				self.sendCommand('S%d' % round(temp))

	def _replace_feedrate(self, cmd):
		if self._feedrate_factor != 1:
			obj = self._regex_feedrate.search(cmd)
			if obj is not None:
				feedrate_cmd = cmd[obj.start():obj.end()]
				if feedrate_cmd in self._feedrate_dict:
					new_feedrate = self._feedrate_dict[feedrate_cmd]
				else:
					self._actual_feedrate = int(feedrate_cmd[1:])
					new_feedrate = round(self._actual_feedrate * self._feedrate_factor)
					# TODO replace with value from printer profile
					if new_feedrate > 5000:
						new_feedrate = 5000
					elif new_feedrate < 30:
						new_feedrate = 30
					self._feedrate_dict[feedrate_cmd] = new_feedrate
			else:
				return cmd
			return cmd.replace(feedrate_cmd, 'F%d' % round(new_feedrate))
		return cmd

	def _replace_intensity(self, cmd):
		if self._intensity_factor != 1:
			obj = self._regex_intensity.search(cmd)
			if obj is not None:
				intensity_cmd = cmd[obj.start():obj.end()]
				if intensity_cmd in self._intensity_dict:
					new_intensity = self._intensity_dict[intensity_cmd]
				else:
					self._actual_intensity = int(intensity_cmd[1:])
					new_intensity = round(self._actual_intensity * self._intensity_factor)
					if new_intensity > 1000:
						new_intensity = 1000
					self._intensity_dict[intensity_cmd] = new_intensity
			else:
				return cmd
			return cmd.replace(intensity_cmd, 'S%d' % round(new_intensity))
		return cmd

	##~~ command handlers
	def _gcode_G1_sending(self, cmd, cmd_type=None):
		cmd = self._replace_feedrate(cmd)
		cmd = self._replace_intensity(cmd)
		return cmd

	def _gcode_G2_sending(self, cmd, cmd_type=None):
		cmd = self._replace_feedrate(cmd)
		cmd = self._replace_intensity(cmd)
		return cmd

	def _gcode_G3_sending(self, cmd, cmd_type=None):
		cmd = self._replace_feedrate(cmd)
		cmd = self._replace_intensity(cmd)
		return cmd

	def _gcode_G01_sending(self, cmd, cmd_type=None):
		return self._gcode_G1_sending(cmd, cmd_type)

	def _gcode_G02_sending(self, cmd, cmd_type=None):
		return self._gcode_G2_sending(cmd, cmd_type)

	def _gcode_G03_sending(self, cmd, cmd_type=None):
		return self._gcode_G3_sending(cmd, cmd_type)

	def _gcode_H_sent(self, cmd, cmd_type=None):
		self._changeState(self.STATE_HOMING)
		return cmd

	def _gcode_X_sent(self, cmd, cmd_type=None):
		self._changeState(self.STATE_HOMING) # TODO: maybe change to seperate $X mode
		return cmd

	def _gcode_Hold_sent(self, cmd, cmd_type=None):
		self._changeState(self.STATE_PAUSED)
		return cmd

	def _gcode_Resume_sent(self, cmd, cmd_type=None):
		self._changeState(self.STATE_PRINTING)
		return cmd

	def sendCommand(self, cmd, cmd_type=None, processed=False):
		cmd = cmd.encode('ascii', 'replace')
		if not processed:
			cmd = process_gcode_line(cmd)
			if not cmd:
				return

		if cmd[0] == "/":
			specialcmd = cmd[1:].lower()
			if "togglestatusreport" in specialcmd:
				if self._status_timer is None:
					self._status_timer = RepeatedTimer(1, self._poll_status)
					self._status_timer.start()
				else:
					self._status_timer.cancel()
					self._status_timer = None
			elif "setstatusfrequency" in specialcmd:
				data = specialcmd[18:]
				try:
					frequency = float(data)
				except ValueError:
					self._log("No frequency setting found! Using 1 sec.")
					frequency = 1
				if self._status_timer is not None:
					self._status_timer.cancel()
				self._status_timer = RepeatedTimer(frequency, self._poll_status)
				self._status_timer.start()
			elif "disconnect" in specialcmd:
				self.close()
			elif "feedrate" in specialcmd:
				data = specialcmd[8:]
				self._set_feedrate_override(int(data))
			elif "intensity" in specialcmd:
				data = specialcmd[9:]
				self._set_intensity_override(int(data))
			else:
				self._log("Command not Found! %s" % cmd)
				self._log("available commands are:")
				self._log("   /togglestatusreport")
				self._log("   /setstatusfrequency <Inteval Sec>")
				self._log("   /feedrate <%>")
				self._log("   /intensity <%>")
				self._log("   /disconnect")
			return

		eepromCmd = re.search("^\$[0-9]+=.+$", cmd)
		if(eepromCmd and self.isPrinting()):
			self._log("Warning: Configuration changes during print are not allowed!")

		self._commandQueue.put(cmd)
		self._send_event.set()

	def selectFile(self, filename, sd):
		if self.isBusy():
			return

		self._currentFile = PrintingGcodeFileInformation(filename)
		eventManager().fire(Events.FILE_SELECTED, {
			"file": self._currentFile.getFilename(),
			"filename": os.path.basename(self._currentFile.getFilename()),
			"origin": self._currentFile.getFileLocation()
		})
		self._callback.on_comm_file_selected(filename, self._currentFile.getFilesize(), False)

	def unselectFile(self):
		if self.isBusy():
			return

		self._currentFile = None
		eventManager().fire(Events.FILE_DESELECTED)
		self._callback.on_comm_file_selected(None, None, False)

	def startPrint(self):
		if not self.isOperational():
			return

		if self._currentFile is None:
			raise ValueError("No file selected for printing")

		# reset feedrate and intesity factor in case they where changed in a previous run
		self._feedrate_factor  = 1
		self._intensity_factor = 1
		self._passes = 1
		self._finished_passes = 0

		try:
			# ensure fan is on whatever gcode follows.
			self.sendCommand("M08")

			self._currentFile.start()
			self._finished_currentFile = False

			payload = {
				"file": self._currentFile.getFilename(),
				"filename": os.path.basename(self._currentFile.getFilename()),
				"origin": self._currentFile.getFileLocation()
			}
			eventManager().fire(Events.PRINT_STARTED, payload)
			#self.sendGcodeScript("beforePrintStarted", replacements=dict(event=payload))

			self._changeState(self.STATE_PRINTING)
		except:
			self._logger.exception("Error while trying to start printing")
			self._errorValue = get_exception_string()
			self._changeState(self.STATE_ERROR)
			eventManager().fire(Events.ERROR, {"error": self.getErrorString()})

	def cancelPrint(self):
		if not self.isOperational():
			return

		with self._commandQueue.mutex:
			self._commandQueue.queue.clear()
		self._soft_reset()
		self._acc_line_buffer = []
		self._send_event.clear(completely=True)
		self._changeState(self.STATE_LOCKED)

		payload = {
			"file": self._currentFile.getFilename(),
			"filename": os.path.basename(self._currentFile.getFilename()),
			"origin": self._currentFile.getFileLocation()
		}

		eventManager().fire(Events.PRINT_CANCELLED, payload)

	def setPause(self, pause, send_cmd=True):
		if not self._currentFile:
			return

		payload = {
			"file": self._currentFile.getFilename(),
			"filename": os.path.basename(self._currentFile.getFilename()),
			"origin": self._currentFile.getFileLocation()
		}

		if not pause and self.isPaused():
			if self._pauseWaitStartTime:
				self._pauseWaitTimeLost = self._pauseWaitTimeLost + (time.time() - self._pauseWaitStartTime)
				self._pauseWaitStartTime = None
			self._pause_delay_time = time.time()
			if send_cmd is True:
				self._real_time_commands['cycle_start']=True
			self._send_event.set()
			eventManager().fire(Events.PRINT_RESUMED, payload)
		elif pause and self.isPrinting():
			if not self._pauseWaitStartTime:
				self._pauseWaitStartTime = time.time()
			self._pause_delay_time = time.time()
			if send_cmd is True:
				self._real_time_commands['feed_hold']=True
			self._send_event.set()
			eventManager().fire(Events.PRINT_PAUSED, payload)

	def increasePasses(self):
		self._passes += 1
		self._log("increased Passes to %d" % self._passes)

	def degreasePasses(self):
		self._passes -= 1
		self._log("degrease Passes to %d" % self._passes)

	def getStateString(self):
		if self._state == self.STATE_NONE:
			return "Offline"
		if self._state == self.STATE_OPEN_SERIAL:
			return "Opening serial port"
		if self._state == self.STATE_DETECT_SERIAL:
			return "Detecting serial port"
		if self._state == self.STATE_CONNECTING:
			return "Connecting"
		if self._state == self.STATE_OPERATIONAL:
			return "Operational"
		if self._state == self.STATE_PRINTING:
			return "Printing"
		if self._state == self.STATE_PAUSED:
			return "Paused"
		if self._state == self.STATE_CLOSED:
			return "Closed"
		if self._state == self.STATE_ERROR:
			return "Error: %s" % (self.getErrorString())
		if self._state == self.STATE_CLOSED_WITH_ERROR:
			return "Error: %s" % (self.getErrorString())
		if self._state == self.STATE_LOCKED:
			return "Locked"
		if self._state == self.STATE_HOMING:
			return "Homing"
		if self._state == self.STATE_FLASHING:
			return "Flashing"
		return "?%d?" % (self._state)

	def getPrintProgress(self):
		if self._currentFile is None:
			return None
		return self._currentFile.getProgress()

	def getPrintFilepos(self):
		if self._currentFile is None:
			return None
		return self._currentFile.getFilepos()

	def getCleanedPrintTime(self):
		printTime = self.getPrintTime()
		if printTime is None:
			return None
		return printTime

	def getConnection(self):
		return self._port, self._baudrate

	def isOperational(self):
		return self._state == self.STATE_OPERATIONAL or self._state == self.STATE_PRINTING or self._state == self.STATE_PAUSED

	def isPrinting(self):
		return self._state == self.STATE_PRINTING

	def isPaused(self):
		return self._state == self.STATE_PAUSED

	def isLocked(self):
		return self._state == self.STATE_LOCKED

	def isHoming(self):
		return self._state == self.STATE_HOMING

	def isFlashing(self):
		return self._state == self.STATE_FLASHING

	def isBusy(self):
		return self.isPrinting() or self.isPaused()

	def isError(self):
		return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR

	def isClosedOrError(self):
		return self._state == self.STATE_ERROR or self._state == self.STATE_CLOSED_WITH_ERROR or self._state == self.STATE_CLOSED

	def isSdReady(self):
		return False

	def isStreaming(self):
		return False

	def getErrorString(self):
		return self._errorValue

	def getPrintTime(self):
		if self._currentFile is None or self._currentFile.getStartTime() is None:
			return None
		else:
			return time.time() - self._currentFile.getStartTime() - self._pauseWaitTimeLost

	def close(self, isError = False):
		if self._status_timer is not None:
			try:
				self._status_timer.cancel()
				self._status_timer = None
			except AttributeError:
				pass

		self._monitoring_active = False
		self._sending_active = False

		printing = self.isPrinting() or self.isPaused()
		if self._serial is not None:
			if isError:
				self._changeState(self.STATE_CLOSED_WITH_ERROR)
			else:
				self._changeState(self.STATE_CLOSED)
			self._serial.close()
		self._serial = None

		if printing:
			payload = None
			if self._currentFile is not None:
				payload = {
					"file": self._currentFile.getFilename(),
					"filename": os.path.basename(self._currentFile.getFilename()),
					"origin": self._currentFile.getFileLocation()
				}
			eventManager().fire(Events.PRINT_FAILED, payload)
		eventManager().fire(Events.DISCONNECTED)

### MachineCom callback ################################################################################################
class MachineComPrintCallback(object):
	def on_comm_log(self, message):
		pass

	def on_comm_temperature_update(self, temp, bedTemp):
		pass

	def on_comm_state_change(self, state):
		pass

	def on_comm_message(self, message):
		pass

	def on_comm_progress(self):
		pass

	def on_comm_print_job_done(self):
		pass

	def on_comm_z_change(self, newZ):
		pass

	def on_comm_file_selected(self, filename, filesize, sd):
		pass

	def on_comm_sd_state_change(self, sdReady):
		pass

	def on_comm_sd_files(self, files):
		pass

	def on_comm_file_transfer_started(self, filename, filesize):
		pass

	def on_comm_file_transfer_done(self, filename):
		pass

	def on_comm_force_disconnect(self):
		pass

	def on_comm_pos_update(self, MPos, WPos):
		pass

class PrintingFileInformation(object):
	"""
	Encapsulates information regarding the current file being printed: file name, current position, total size and
	time the print started.
	Allows to reset the current file position to 0 and to calculate the current progress as a floating point
	value between 0 and 1.
	"""

	def __init__(self, filename):
		self._logger = logging.getLogger(__name__)
		self._filename = filename
		self._pos = 0
		self._size = None
		self._start_time = None

	def getStartTime(self):
		return self._start_time

	def getFilename(self):
		return self._filename

	def getFilesize(self):
		return self._size

	def getFilepos(self):
		return self._pos

	def getFileLocation(self):
		return FileDestinations.LOCAL

	def getProgress(self):
		"""
		The current progress of the file, calculated as relation between file position and absolute size. Returns -1
		if file size is None or < 1.
		"""
		if self._size is None or not self._size > 0:
			return -1
		return float(self._pos) / float(self._size)

	def reset(self):
		"""
		Resets the current file position to 0.
		"""
		self._pos = 0

	def start(self):
		"""
		Marks the print job as started and remembers the start time.
		"""
		self._start_time = time.time()

	def close(self):
		"""
		Closes the print job.
		"""
		pass

class PrintingGcodeFileInformation(PrintingFileInformation):
	"""
	Encapsulates information regarding an ongoing direct print. Takes care of the needed file handle and ensures
	that the file is closed in case of an error.
	"""

	def __init__(self, filename, offsets_callback=None, current_tool_callback=None):
		PrintingFileInformation.__init__(self, filename)

		self._handle = None

		self._first_line = None

		self._offsets_callback = offsets_callback
		self._current_tool_callback = current_tool_callback

		if not os.path.exists(self._filename) or not os.path.isfile(self._filename):
			raise IOError("File %s does not exist" % self._filename)

		self._stripCommments()

		self._size = os.stat(self._filename).st_size
		self._pos = 0

	def start(self):
		"""
		Opens the file for reading and determines the file size.
		"""
		PrintingFileInformation.start(self)
		self._handle = open(self._filename, "r")

	def close(self):
		"""
		Closes the file if it's still open.
		"""
		PrintingFileInformation.close(self)
		if self._handle is not None:
			try:
				self._handle.close()
			except:
				pass
		self._handle = None

	def resetToBeginning(self):
		"""
		resets the file handle so you can read from the beginning again.
		"""
		self._handle = open(self._filename, "r")

	def getNext(self):
		"""
		Retrieves the next line for printing.
		"""
		if self._handle is None:
			raise ValueError("File %s is not open for reading" % self._filename)

		try:
			processed = None
			while processed is None:
				if self._handle is None:
					# file got closed just now
					return None
				line = self._handle.readline()
				if not line:
					self.close()
				processed = process_gcode_line(line)
			self._pos = self._handle.tell()

			return processed
		except Exception as e:
			self.close()
			self._logger.exception("Exception while processing line")
			raise e

	def _stripCommments(self):
		dir     = os.path.dirname(os.path.abspath(self._filename))
		tmpfile = open(dir + '/gcode.tmp', 'w')
		with open(self._filename, "r") as fileobject:
			for line in fileobject:
				if process_gcode_line(line) is not None:
					tmpfile.write(line)
		tmpfile.close()
		self._filename = dir + '/gcode.tmp'

def convert_pause_triggers(configured_triggers):
	triggers = {
		"enable": [],
		"disable": [],
		"toggle": []
	}
	for trigger in configured_triggers:
		if not "regex" in trigger or not "type" in trigger:
			continue

		try:
			regex = trigger["regex"]
			t = trigger["type"]
			if t in triggers:
				# make sure regex is valid
				re.compile(regex)
				# add to type list
				triggers[t].append(regex)
		except re.error:
			# invalid regex or something like this, we'll just skip this entry
			pass

	result = dict()
	for t in triggers.keys():
		if len(triggers[t]) > 0:
			result[t] = re.compile("|".join(map(lambda pattern: "({pattern})".format(pattern=pattern), triggers[t])))
	return result

def process_gcode_line(line):
	line = strip_comment(line).strip()
	line = line.replace(" ", "")
	if not len(line):
		return None
	return line

def strip_comment(line):
	if not ";" in line:
		# shortcut
		return line

	escaped = False
	result = []
	for c in line:
		if c == ";" and not escaped:
			break
		result += c
		escaped = (c == "\\") and not escaped
	return "".join(result)

def get_new_timeout(t):
	now = time.time()
	return now + get_interval(t)

def get_interval(t):
	if t not in default_settings["serial"]["timeout"]:
		return 0
	else:
		return settings().getFloat(["serial", "timeout", t])

def serialList():
	baselist = []
	baselist = baselist \
				+ glob.glob("/dev/ttyUSB*") \
				+ glob.glob("/dev/ttyACM*") \
				+ glob.glob("/dev/ttyAMA*") \
				+ glob.glob("/dev/tty.usb*") \
				+ glob.glob("/dev/cu.*") \
				+ glob.glob("/dev/cuaU*") \
				+ glob.glob("/dev/rfcomm*")

	additionalPorts = settings().get(["serial", "additionalPorts"])
	for additional in additionalPorts:
		baselist += glob.glob(additional)

	prev = settings().get(["serial", "port"])
	if prev in baselist:
		baselist.remove(prev)
		baselist.insert(0, prev)
	if settings().getBoolean(["devel", "virtualPrinter", "enabled"]):
		baselist.append("VIRTUAL")
	return filter(None, baselist)

def baudrateList():
	ret = [250000, 230400, 115200, 57600, 38400, 19200, 9600]
	prev = settings().getInt(["serial", "baudrate"])
	if prev in ret:
		ret.remove(prev)
		ret.insert(0, prev)
	return ret
