#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
| `@purpose`: Interface for the SR Research Eyelink eyetracking system.  
| `@date`: Created on Wed Feb 13 15:37:43 2019  
| `@author`: Semeon Risom  
| `@email`: semeon.risom@gmail.com  
| `@url`: https://semeon.io/d/imhr
""" 
# allowed imports
__all__ = ['Eyelink']

# required external libraries
__required__ = ['psychopy','pandas']

# debug
from pdb import set_trace as breakpoint

# local libraries
from .. import settings
from . import pylink

# check if psychopy is available
try:
	# core
	import time
	import os
	import re
	import psychopy
	import platform
	import pandas as pd
	from pathlib import Path
except ImportError as e:
	pkg = e.name
	x = {'psychopy':'psychopy','pandas':'pandas'}
	pkg = x[pkg] if pkg in x else pkg
	raise Exception("No module named '%s'. Please install from PyPI before continuing."%(pkg),'red')

class Eyelink():
	"""Interface for the SR Research Eyelink eyetracking system."""
	def __init__(self, window, timer, isPsychopy=True, subject=None, **kwargs):
		"""Interface for the SR Research Eyelink eyetracking system..

		Parameters
		----------
		window :  `psychopy.visual.Window <https://www.psychopy.org/api/visual/window.html#window>`_
			PsychoPy window instance.
		timer :  `psychopy.core.CountdownTimer <https://www.psychopy.org/api/core.html#psychopy.core.CountdownTimer>`_
			Psychopy timer instance.
		isPsychopy : :obj:`bool`
			Is Psychopy being used. Default `True`.
		subject : :obj:`int`
			Subject number.
		**kwargs : :obj:`str` or :obj:`None`, optional
			Here's a list of available properties:

			.. list-table::
				:class: kwargs
				:widths: 25 50
				:header-rows: 1

				* - Property
				  - Description
				* - **isFlag** : :obj:`bool`
				  - Bypass Eyelink flags (`isRecording`, `isConnected`) to run all functions without checking flags. Default is `True`.
				* - **isLibrary** : :obj:`bool`
				  - Check if required packages have been installed. Default is `False`.
				* - **demo** : :obj:`bool`
				  - Run demo mode, which includes region of interest highlighting and other testing methods. Default is `False`.

		Examples
		--------
		>>> eytracking = imhr.eyetracking(window=window, subject=subject)

		Notes
		-----
		According to `pylink.chm <../manual/pylink.chm>`_, the sequence of operations for implementing a trial is:
			1) Perform a DRIFT CORRECTION, which also serves as the pre-trial fixation target.
			2) Start recording, allowing 100 milliseconds of data to accumulate before the trial display starts.
			3) Draw the subject display, recording the time that the display appeared by placing a message in the EDF file.
			4) Loop until one of these events occurs RECORDING halts, due to the tracker ABORT menu or an error, the maximum \
			trial duration expires 'ESCAPE' is pressed, the program is interrupted, or abutton on the EyeLink button box is pressed.
			5) Add special code to handle gaze-contingent display updates.
			6) Blank the display, stop recording after an additional 100 milliseconds of data has been collected.
			7) Report the trial result, and return an appropriate error code.
		"""
		#----imports
		# psychopy
		from psychopy import visual, core, event
		from psychopy.constants import (NOT_STARTED, STARTED, FINISHED)

		#----constants
		# core
		self.console = settings.console
		# psychopy
		self.visual = visual
		self.core = core
		self.event = event
		self.STARTED = STARTED
		self.FINISHED = FINISHED
		self.NOT_STARTED = NOT_STARTED
		
		#----introduction
		self.console("imhr.eyetracking() found.",'green')

		#----screen size
		if isPsychopy:
			self.w = int(window.size[0])
			self.h = int(window.size[1])
		else:
			# if windows
			if platform.system() == "Windows":
				## try to import
				try:
					from win32api import GetSystemMetrics
				except ImportError:
					self.console("No module named 'win32api'. Installing from PyPI",'red')
					settings.library(['win32api'])
				# get width, height
				self.w = int(GetSystemMetrics(0))
				self.h = int(GetSystemMetrics(1))
				self.console("imhr.eyetracking(): screensize [%s, %s]."%(self.w, self.h))
			# else if osx
			elif platform.system() == 'Darwin':
				## try to import
				try:
					import pyobjc
					from AppKit import NSScreen
				except ImportError:
					self.console("No module named 'pyobjc'. Installing from PyPI",'red')
					settings.library(['pyobjc'])
				# get width, height
				self.w = int(NSScreen.mainScreen().frame().size.width)
				self.h = int(NSScreen.mainScreen().frame().size.height)
				self.console("imhr.eyetracking(): screensize [%s, %s]."%(self.w, self.h))

		#----parameters
		# instants
		self.window = window
		self.Calibration = ''
		# flags
		self.isConnected = False
		self.isStarted = False
		self.isCalibration = False
		self.isRecording = False
		self.isDriftCorrection = False
		self.isFinished = False
		# counters
		self.drift_count = 0
		# metadata
		self.trial = ''
		self.block = ''
		# pupil
		self.eye_used = None
		self.left_eye = 0
		self.right_eye = 1
		# status
		self.status = {}

		#----timing
		self.routineTimer = timer

		#----drift correction
		#clock
		self.drift_message_clock = core.Clock()
		self.drift_clock = core.Clock()
		#screen
		text = '\n'.join([
			"On the next screen a fixation dot will appear.",
			"Please look at the fixation dot until it disappears.",
			"\nPress any key to continue."
		])
		self.drift_text = visual.TextStim(win=window, name='drift_message', font='Helvetica',
		text=text, height=0.1, wrapWidth=1.5, ori=0, pos=(0, 0), alignVert='center', color='black', 
		colorSpace='rgb', opacity=1, depth=0.0)

		# check if subject number has been entered
		if subject == None:
			raise AssertionError('Subject number not entered. Please enter the subject number.')
		else:
			self.subject = subject

		#----edf filename
		self.subject = os.path.splitext(str(subject))[0]
		# check if integer
		if re.match(r'\w+$', self.subject):
			pass
		else:
			self.window.close()
			raise AssertionError('Name must only include A-Z, 0-9, or _')
		# check length
		if len(self.subject) <= 8:
			pass
		else:
			self.window.close()
			raise AssertionError('Name must be <= 8 characters.')
		# store name
		self.fname = str(self.subject) + '.edf'

		# generate file path
		self.path = "%s/data/edf/" % (os.getcwd())
		if not os.path.exists(self.path):
			os.makedirs(self.path)

		#----kwargs
		# demo
		self.isDemo = kwargs['isDemo'] if 'isDemo' in kwargs else False
		# turn off eyelink flags
		self.isFlag = kwargs['isFlag'] if 'isFlag' in kwargs else True
		# isLibrary
		self.isLibrary = kwargs['isLibrary'] if 'isLibrary' in kwargs else False

		#----check if required libraries are available
		if self.isLibrary:
			settings.library(__required__)

		#----kwargs

	def connect(self, calibration_type=13, automatic_calibration_pacing=1000, saccade_velocity_threshold=35,
				saccade_acceleration_threshold=9500, sound=True, select_parser_configuration=0,
				recording_parse_type="GAZE", enable_search_limits=True, ip='100.1.1.1'):
		"""
		Connect to Eyelink.

		Parameters
		----------
		ip : :obj:`string`
			Host PC ip address.
		calibration_type : :obj:`int`
			Calibration type. Default is 13-point. [see `Eyelink 1000 Plus User Manual, 3.7 Calibration \
			<../manual/EyeLink%201000%20Plus%20User%20Manual%201.0.12.pdf>`_]
		automatic_calibration_pacing : :obj:`int`
			Select the delay in milliseconds, between successive calibration or validation targets 
			if automatic target detection is activeSet automatic calibration pacing. [see `pylink.chm <../manual/pylink.chm>`_]
		saccade_velocity_threshold : :obj:`int`
			Sets velocity threshold of saccade detector: usually 30 for cognitive research, 22 for 
			pursuit and neurological work. Default is 35. Note: EyeLink II and EyeLink 1000,
			select select_parser_configuration should be used instead. [see `EyeLink Programmer’s Guide, \
			Section 25.9: Parser Configuration <../manual/EyeLink%20Programmers%20Guide.pdf>`_; `Eyelink 1000 Plus \
			User Manual, Section 4.3.5 Saccadic Thresholds <../manual/EyeLink%201000%20Plus%20User%20Manual%201.0.12.pdf>`_]
		saccade_acceleration_threshold : :obj:`int`
			Sets acceleration threshold of saccade detector: usually 9500 for cognitive research, 5000 
			for pursuit and neurological work. Default is 9500. Note: For EyeLink II and EyeLink 1000,
			select select_parser_configuration should be used instead. [see `EyeLink Programmer’s Guide, \
			Section 25.9: Parser Configuration <../manual/EyeLink%20Programmers%20Guide.pdf>`_; \
			`Eyelink 1000 Plus User Manual, Section 4.3.5 Saccadic Thresholds <../manual/EyeLink%201000%20Plus%20User%20Manual%201.0.12.pdf>`_]
		select_parser_configuration : :obj:`int`
			Selects the preset standard parser setup (0) or more sensitive (1). These are equivalent
			to the cognitive and psychophysical configurations. Default is 0. [see `EyeLink Programmer’s \
			Guide, Section 25.9: Parser Configuration <../manual/EyeLink%20Programmers%20Guide.pdf>`_]
		sound : :obj:`bool`
			Should sound be used for calibration/validation/drift correction.
		recording_parse_type : :obj:`str`
			Sets how velocity information for saccade detection is to be computed.
			Enter either 'GAZE' or 'HREF'. Default is 'GAZE'. [see `Eyelink 1000 Plus User Manual, \
			Section 4.4: File Data Types <../manual/EyeLink%201000%20Plus%20User%20Manual%201.0.12.pdf>`_]
		enable_search_limits : :obj:`bool`
			Enables tracking of pupil to global search limits. Default is True. [see `Eyelink 1000 Plus User Manual, \
			Section 4.4: File Data Types <../manual/EyeLink%201000%20Plus%20User%20Manual%201.0.12.pdf>`_]

		Returns
		----------
		param : :class:`pandas.DataFrame`
			Returns dataframe of parameters for subject.

		Examples
		--------
		>>> param = eyetracking.connect(calibration_type=13)
		"""
		#----initiate connection with eyetracker
		self.ip = ip
		try:
			from . import pylink
			self.tracker = pylink.EyeLink(self.ip)
			self.isConnected = True
			self.console("Eyelink Connected", "blue")
		except RuntimeError:
			self.tracker = None
			self.isConnected = False
			self.console("Eyelink not detected at %s"%(self.ip), 'red')

		#----tracker metadata
		# get eyelink version
		self.tracker_version = self.tracker.getTrackerVersion()

		# get host tracking software version
		self.host_version = 0
		if self.tracker_version == 3:
			tvstr = self.tracker.getTrackerVersionString()
			vindex = tvstr.find("EYELINK CL")
			self.host_version = int(
				float(tvstr[(vindex + len("EYELINK CL")):].strip()))

		#----preset
		self.select_parser_configuration = select_parser_configuration
		self.saccade_acceleration_threshold = saccade_acceleration_threshold
		self.saccade_velocity_threshold = saccade_velocity_threshold
		self.recording_parse_type = recording_parse_type
		self.enable_search_limits = "YES" if enable_search_limits else "NO"
		# calibration
		self.calibration_type = calibration_type  # []-point calibration
		self.automatic_calibration_pacing = automatic_calibration_pacing
		# specify the EVENT and SAMPLE data that are stored in EDF or retrievable from the Link
		# [see Section 4.6 Data Files of the  EyeLink 1000 Plus user manual]
		self.fef = "LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON,INPUT"
		self.lef = "LEFT,RIGHT,FIXATION,FIXUPDATE,SACCADE,BLINK,BUTTON,INPUT"
		if self.host_version >= 4:
			self.fsd = "LEFT,RIGHT,GAZE,AREA,GAZERES,STATUS,HTARGET,INPUT"
			self.lsd = "LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS,HTARGET,INPUT"
		else:
			self.fsd = "LEFT,RIGHT,GAZE,AREA,GAZERES,STATUS"
			self.lsd = "LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS"

		#---store param for export
		self.param = dict(
			tracker_version=self.tracker_version, 
			host_version=self.host_version,
			select_parser_configuration=self.select_parser_configuration,
			saccade_acceleration_threshold=self.saccade_acceleration_threshold,
			saccade_velocity_threshold=self.saccade_velocity_threshold,
			recording_parse_type=self.recording_parse_type,
			enable_search_limits=self.enable_search_limits,
			automatic_calibration_pacing=self.automatic_calibration_pacing,
			file_event_filter=self.fef,
			file_sample_data=self.fsd,
			link_event_filter=self.lef,
			link_sample_data=self.lsd,
			calibration_type=self.calibration_type
		)

		#----open edf
		self.tracker.openDataFile(self.fname)
		pylink.flushGetkeyQueue()

		#----send settings to eyelink
		# place EyeLink tracker in offline (idle) mode before changing settings
		self.tracker.setOfflineMode()

		# Set the tracker to parse events using either GAZE or HREF
		# note: [see `Data Viewer User Manual, Section 7: Protocol for EyeLink Data to Viewer Integration <../manual/EyeLink%20Data%20Viewer%20User%20Manual%203.2.1.pdf>`_]
		self.tracker.sendCommand("recording_parse_type = %s" %(self.recording_parse_type))
   
		# inform the tracker the resolution of the subject display
		# note: [see Eyelink Installation Guide, Section 8.4: Customizing Your PHYSICAL.INI Settings]
		self.tracker.sendCommand("screen_pixel_coords = 0 0 %d %d" % (self.w - 1, self.h - 1))
   
		# save display resolution in EDF data file for Data Viewer integration purposes
		# note: [see `Data Viewer User Manual, Section 7: Protocol for EyeLink Data to Viewer Integration <../manual/EyeLink%20Data%20Viewer%20User%20Manual%203.2.1.pdf>`_]
		self.tracker.sendMessage("DISPLAY_COORDS = 0 0 %d %d" % (self.w - 1, self.h - 1))

		# set calibration type
		self.tracker.sendCommand("calibration_type = HV%d"%(self.calibration_type))
		# set automatic calibraiton pacing interval
		self.tracker.sendCommand("automatic_calibration_pacing = %d" % (
			self.automatic_calibration_pacing))

		# if using tracker version 2 or greater
		if self.tracker_version >= 2:
			self.tracker.sendCommand("select_parser_configuration %d" % (
				self.select_parser_configuration))
			# turn off scene link
			if self.tracker_version == 2:
				self.tracker.sendCommand("scene_camera_gazemap = NO")
		else:
			self.tracker.sendCommand("saccade_velocity_threshold = %s" % (
				self.saccade_velocity_threshold))
			self.tracker.sendCommand("saccade_acceleration_threshold = %s" % (
				self.saccade_acceleration_threshold))

		# if using tracker version 3 or above
		if self.tracker_version >= 3:
			self.tracker.sendCommand(
				"enable_search_limits=%s" % (self.enable_search_limits))
			self.tracker.sendCommand("track_search_limits=YES")
			self.tracker.sendCommand("autothreshold_click=YES")
			self.tracker.sendCommand("autothreshold_repeat=YES")
			self.tracker.sendCommand("enable_camera_position_detect=YES")

		# set content of edf file
		# edf filters #event types
		self.tracker.sendCommand('file_event_filter = %s' % (self.fef))
		self.tracker.sendCommand('file_sample_data = %s' % (self.fsd))
		self.tracker.sendCommand('link_event_filter = %s' % (self.lef))
		self.tracker.sendCommand('link_sample_data = %s' % (self.lsd))

		# program button '5' for drift correction
		self.tracker.sendCommand("button_function 5 'accept_target_fixation'")

		# select sound for calibration and drift correct
		# "" = sound, "off" = no sound
		pylink.setCalibrationSounds("", "", "")
		pylink.setDriftCorrectSounds("", "", "")

		# export param to txt file
		## prevent metadata from being truncated
		width = pd.get_option('display.max_colwidth')
		pd.set_option('display.max_colwidth', -1)
		# create df
		param = pd.DataFrame(list(self.param.items()),columns=['category', 'value'])
		param.to_csv(path_or_buf=self.path + str(self.subject) + ".txt", index=True, index_label='index')
		# reset truncation
		pd.set_option('display.max_colwidth', width)

		#if ipython
		# from IPython.display import display
		# display(param)

		return param

	def set_eye_used(self, eye):
		"""
		Set dominant eye. This step is required for recieving gaze coordinates from Eyelink->Psychopy.

		Parameters
		----------
		eye : :obj:`str`
			Dominant eye (left, right). This will be used for outputting Eyelink gaze samples.

		Examples
		--------
		>>> dominant_eye = 'left'
		>>> eye_used = eyetracking.set_eye_used(eye=dominant_eye)
		"""
		self.console("eyetracking.set_eye_used()")
		eye_entered = str(eye)
		if eye_entered in ('Left', 'LEFT', 'left', 'l', 'L'):
			self.console("eye_entered = %s('left')" %(eye_entered), 'blue')
			self.eye_used = self.left_eye
		else:
			self.console("eye_entered = %s('right')" %(eye_entered), 'blue')
			self.eye_used = self.right_eye

		return self.eye_used

	def calibration(self):
		"""
		Start calibration procedure.

		Returns
		-------
		isCalibration : :obj:`bool`
			Message indicating status of calibration.

		Examples
		--------
		>>> eyetracking.calibration()
		"""
		# calibration
		from . import pylink
		from . import Calibration

		self.console("eyetracking.calibration()")

		#----if connected to eyetracker
		if self.isConnected or (not self.isFlag):
			# put the tracker in offline mode before we change its configurations
			self.tracker.setOfflineMode()
			# Generate custom calibration stimuli
			self.Calibration = Calibration(w=self.w, h=self.h, tracker=self.tracker, window=self.window)
			# execute custom calibration display
			pylink.openGraphicsEx(self.Calibration)
			# calibrate
			self.tracker.doTrackerSetup(self.w, self.h)
			#flip screen after finishing
			self.window.flip()

		#----finished
		self.isCalibration = True
		self.console("eyetracking.calibration() finished", 'blue')

		return self.Calibration

	def _drift_message(self):
		#----prepare to start Routine 'drift_message'
		endExpNow = False  # flag for 'escape' or other condition => quit the exp
		t = 0
		self.drift_message_clock.reset()  # clock
		frameN = -1
		continueRoutine = True
		# update component parameters for each repeat
		drift_response = self.event.BuilderKeyResponse()
		# keep track of which components have finished
		drift_message_components = [drift_response, self.drift_text]
		for thisComponent in drift_message_components:
			if hasattr(thisComponent, 'status'):
				thisComponent.status = self.NOT_STARTED

		# -------Start Routine "drift_message"-------
		while continueRoutine:
			# get current time
			t = self.drift_message_clock.getTime()
			frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
			# update/draw components on each frame

			# *drift_response* updates
			if t >= 0.0 and drift_response.status == self.NOT_STARTED:
				# keep track of start time/frame for later
				drift_response.tStart = t
				drift_response.frameNStart = frameN  # exact frame index
				drift_response.status = self.STARTED
				# keyboard checking is just starting
				self.event.clearEvents(eventType='keyboard')
			if drift_response.status == self.STARTED:
				theseKeys = self.event.getKeys()

				# check for quit:
				if "escape" in theseKeys:
					endExpNow = True
				if len(theseKeys) > 0:  # at least one key was pressed
					# a response ends the routine
					continueRoutine = False

			# *drift_text* updates
			if t >= 0.0 and self.drift_text.status == self.NOT_STARTED:
				# keep track of start time/frame for later
				self.drift_text.tStart = t
				self.drift_text.frameNStart = frameN  # exact frame index
				self.drift_text.setAutoDraw(True)

			# check for quit (typically the Esc key)
			if endExpNow or self.event.getKeys(keyList=["escape"]):
				self.core.quit()

			# check if all components have finished
			if not continueRoutine:  # a component has requested a forced-end of Routine
				break
			continueRoutine = False  # will revert to True if at least one component still running
			for thisComponent in drift_message_components:
				if hasattr(thisComponent, "status") and thisComponent.status != self.FINISHED:
					continueRoutine = True
					break  # at least one component has not yet finished

			# refresh the screen
			if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
				self.window.flip()

		# -------Ending Routine "drift_message"-------
		for thisComponent in drift_message_components:
			if hasattr(thisComponent, "setAutoDraw"):
				thisComponent.setAutoDraw(False)

		# the Routine "drift_message" was not non-slip safe, so reset the non-slip timer
		self.routineTimer.reset()

	def drift_correction(self, origin='call'):
		"""
		Starts drift correction. This can be done at any point after calibration, including before 
		and after eyetracking.start_recording has already been initiated.

		Parameters
		----------
		origin : :obj:`str`
			Origin of call, either `manual` (default) or from gaze contigent event (`gc`).

		Returns
		-------
		isDriftCorrection : :obj:`bool`
			Message indicating status of drift correction.

		Notes
		-----
		Running drift_correction will end any start_recording event to function properly. Once drift
		correction has occured, it is safe to run start_recording.

		Examples
		--------
		>>> eyetracking.drift_correction()
		"""
		self.console("eyetracking.drift_correction()")

		#----present drift correction message (only if manually accessing)
		if origin=='call':
			self._drift_message()
			self.window.clearBuffer()
			self.window.flip()

		#check if recording
		if self.isRecording or (not self.isFlag):
			# end of trial message
			self.tracker.sendMessage('drift correction')

			# end realtime mode
			pylink.endRealTimeMode()
			pylink.msecDelay(100)

			# send trial-level variables
			#if running from self.gc
			if origin=='gc':
				self.send_variable(variables=dict(trial=self.trial, block=self.block, issues='gc window failed'))

			# specify end of trial
			self.tracker.sendMessage("TRIAL_RESULT 1")

			# stop recording eye data
			self.tracker.stopRecording()

		# flag isRecording
		self.isRecording = False

		# initiate drift correction, flag isDriftCorrection
		self.isDriftCorrection = True
		try:
		   self.tracker.doDriftCorrect(int(self.w/2), int(self.h/2), 1, 1)
		except:
		   self.tracker.doTrackerSetup()

		#----finished
		self.console("eyetracking.drift_correction() finished", "blue")

		return self.isDriftCorrection

	def start_recording(self, trial, block):
		"""
		Starts recording of Eyelink.

		Parameters
		----------
		trial : :obj:`str`
			Trial Number.
		block : :obj:`str`
			Block Number.

		Returns
		-------
		isRecording : :obj:`bool`
			Message indicating status of Eyelink recording.

		Notes
		-----
		tracker.beginRealTimeMode():
			To ensure that no data is missed before the important part of the trial starts. The EyeLink 
			tracker requires 10 to 30 milliseconds after the recording command to begin writing data. 
			This extra data also allows the detection of blinks or saccades just before the trial start, 
			allowing bad trials to be discarded in saccadic RT analysis. A "SYNCTIME" message later 
			in the trial marks the actual zero-time in the trial's data record for analysis.
			[see `pylink.chm <../manual/pylink.chm>`_]
		TrialID:
			The "TRIALID" message is sent to the EDF file next. This message must be placed 
			in the EDF file before the drift correction and before recording begins, and is critical 
			for data analysis. The viewer will not parse any messages, events, or samples that exist
			in the data file prior to this message. The command identifier can be changed in the data
			loading preference settings.
			[see `Data Viewer User Manual, Section 7: Protocol for EyeLink Data to Viewer Integration \
			<../manual/EyeLink%20Data%20Viewer%20User%20Manual%203.2.1.pdf>`_]
		SYNCTIME:
			Marks the zero-time in a trial. A number may follow, which is interpreted as the delay of 
			the message from the actual stimulus onset. It is suggested that recording start 100 
			milliseconds before the display is drawn or unblanked at zero-time, so that no data at the 
			trial start is lost.
			[see `pylink.chm <../manual/pylink.chm>`_]

		Examples
		--------
		>>> eyetracking.start_recording(trial=1, block=1)
		"""
		self.console("eyetracking.start_recording()")

		# indicates start of trial
		self.tracker.sendMessage('TRIALID %s' % (str(trial)))

		# Message to post to Eyelink display Monitor
		self.tracker.sendCommand("record_status_message 'Trial %s Block %s'" % (str(trial), str(block)))

		# start recording, parameters specify whether events and samples are
		# stored in file, and available over the link
		self.tracker.startRecording(1, 1, 1, 1)

		# buffer to prevent loss of data
		pylink.beginRealTimeMode(100)

		# indicates zero-time of trial
		self.tracker.sendMessage('SYNCTIME')
		self.tracker.sendMessage('start recording')

		# flag isDriftCorrection, isRecording, trial, block
		self.isDriftCorrection = False
		self.isRecording = True
		self.trial = trial
		self.block = block

		return self.isRecording

	def gc(self, image=None, text=None, bound=None, min_=None, max_=None, **kwargs):
		"""
		Creates gaze contigent event. Note: This function needs to be run while recording.

		Parameters
		----------
		image : :obj:`str` or :obj:`None`
			Image to display on screen during gc event (if type = image).
		text : :obj:`str` or :obj:`None`
			Text to display on screen during gc event (if type = text).
		bound : :obj:`dict` [:obj:`str`, :obj:`int`] or :obj:`None`:
			Dictionary of the bounding box for each region of interest. Keys are each side of the 
			bounding box and values are their corresponding coordinates in pixels.
		min_ : :obj:`int`
			Mininum duration (msec) in which gaze contigent capture is collecting data before allowing
			the task to continue.
		max_ : :obj:`int` or :obj:`None`
			Maxinum duration (msec) before task is forced to go into drift correction. 
		**kwargs : :obj:`str` or :obj:`None`, optional
			Here's a list of available properties:

			.. list-table::
				:class: kwargs
				:widths: 25 50
				:header-rows: 1

				* - Property
				  - Description
				* - **font** : :obj:`str`
				  - Stimulus font. Default is `Arial`.
				* - **name** : :obj:`str`
				  - Stimulus name. Default is `fixation`.
				* - **pos** : :obj:`list`
				  - Stimulus position. Default is `[0, 0]`.
				* - **size** : :obj:`list`
				  - Stimulus image size. Default is `[100, 100]`.
				* - **height** : :obj:`float`
				  - Stimulus text height. Default is `0.13`.
				* - **color** : :obj:`str`
				  - Stimulus text color. Default is `black`.
				* - **opacity** : :obj:`int`
				  - Stimulus text opacity. Default is `1`.
				* - **depth** : :obj:`float`
				  - Stimulus text depth. Default is `0.0`.

		Examples
		--------
		>>> # Collect samples within the center of the screen, for 2000 msec, 
		>>> # with a max limit of 10000 msec.
		>>> region = dict(left=860, top=440, right=1060, bottom=640)
		>>> eyetracking.gc(bound=bound, min_=2000, max_=10000)
		"""
		# kwargs
		font = kwargs['font'] if 'font' in kwargs else 'Arial'
		name = kwargs['name'] if 'name' in kwargs else "fixation"
		pos = kwargs['pos'] if 'pos' in kwargs else [2, 0]
		size = kwargs['size'] if 'size' in kwargs else [100, 100]
		height = kwargs['height'] if 'height' in kwargs else 0.13
		color = kwargs['color'] if 'color' in kwargs else 'black'
		opacity = kwargs['opacity'] if 'opacity' in kwargs else 1
		depth = kwargs['depth'] if 'depth' in kwargs else 0.0
		# constants
		status = 'Started'
		self.console("eyetracking.gc() started", "blue")

		#if eyetracker is recording
		if self.isRecording or (not self.isFlag):
			# fixation box (if demo)
			if self.isDemo:
				line = 6
				width = (bound['right'] - bound['left']) + line
				height = (bound['bottom'] - bound['top']) + line
				color = [255,0,0]
				box = self.visual.Rect(win=self.window, name="bound", units='pix',
									width=width, height=height, pos=(0,0), colorSpace='rgb255',
									lineWidth=6, lineColor=color, lineColorSpace='rgb255',
									opacity=1, depth=0.0, interpolate=True)
				box.setAutoDraw(True)


			#fixation
			if image is not None:
				# image
				fixation = self.visual.ImageStim(win=self.window, name=name, units="pix",
					image=image, pos=pos, size=size)
			else:
				# text
				if text is None:
					text = "+"
				fixation = self.visual.TextStim(win=self.window, name=name, alignVert='center', 
					alignHoriz='center', text=text, font=font, pos=pos, height=height, 
					wrapWidth=2, ori=0, color=color, colorSpace='rgb255', opacity=opacity, 
					depth=depth)
			# display
			fixation.setAutoDraw(True)

			# finish
			self.window.flip()
			# draw box on EyeLink display
			#[see `EyeLink Programmer’s Guide, Chapter 15: Tracker Feedback Graphics <../manual/EyeLink%20Programmers%20Guide.pdf>`_]
			self.tracker.sendCommand('draw_filled_box %d %d %d %d 15'% (bound['left'],bound['top'],bound['right'],bound['bottom']))

			#get start time
			start_time = time.clock()

			#get current time
			current_time = time.clock()

			#----check gc window
			while True:

				#get gaze sample
				gxy, ps, s = self.sample()

				#is gaze with gaze contigent window for min_ time
				if ((bound['left'] < gxy[0] < bound['right']) and (bound['top'] < gxy[1] < bound['bottom'])):
					# if demo
					if self.isDemo:
						box.lineColor = [255,0,0]
					# has mininum time for gc window occured
					duration = (time.clock() - current_time) * 1000
					if duration > min_:
						self.console("eyetracking.gc() success in %d msec"%((time.clock() - start_time) * 1000), "blue")
						self.send_message(msg='gc window success')
						# clear bounding box 
						self.tracker.sendCommand('clear_screen 0')
						# clear fixation
						fixation.setAutoDraw(False)
						# (if demo)
						if self.isDemo:
							box.setAutoDraw(False)
						# finished-success
						status = 'Finished'
						break 
				# not in window
				else:
					# if demo
					if self.isDemo:
						box.lineColor = [255,0,0]
					#reset current time
					current_time = time.clock()

				# if reached maxinum time
				if max_ is not None:
					# has maxinum time for gc window occured
					duration = (time.clock() - start_time) * 1000
					if duration > max_:
						self.console("eyetracking.gc() failed, drift correction started", "blue")
						self.send_message(msg='gc window failed')
						self.tracker.sendCommand('clear_screen 0')
						# clear fixation
						fixation.setAutoDraw(False)
						# clear bounding box (if demo)
						if self.isDemo:
							box.setAutoDraw(False)
						# start drift correction
						self.drift_correction(origin='gc')
						status = 'Finished'
						break
			
			return status
		else:
			self.console("eyetracker not recording", "red")
			status = 'Finished'
			return status
 
	def sample(self):
		"""
		Collects new gaze coordinates from Eyelink.

		Returns
		-------
		gxy : :class:`tuple`
			Gaze coordiantes.
		ps : :obj:`tuple`
			Pupil size (area).
		sample : :class:`EyeLink.getNewestSample`
			Eyelink newest sample.

		Examples
		--------
		>>> eyetracking.sample(eye_used=eye_used)
		"""
		# check for new sample update
		sample = self.tracker.getNewestSample()

		#if sample available
		if(sample != None):
			# if dominant eye entered as right in both psychopy and eyelink
			if ((self.eye_used == self.right_eye) and (sample.isRightSample())):
				gxy = sample.getRightEye().getGaze()
				ps = sample.getRightEye().getPupilSize()
			elif ((self.eye_used == self.left_eye) and (sample.isLeftSample())):
				gxy = sample.getLeftEye().getGaze()
				ps = sample.getLeftEye().getPupilSize()
			else:
				 raise AssertionError('eye_used (%s) ≠ Eyelink "Eye Tracked" (%s). \
									  Please make sure the eye used in PsychoPy and Eyelink match.'\
									  %(self.eye_used, sample.getEye()))

			# gaze coordinates, pupil area    
			return gxy, ps, sample

		return None, None, sample

	def send_message(self, msg):
		"""
		Send message to Eyelink. This allows post-hoc processing of event markers (i.e. "stimulus onset").

		Parameters
		----------
		msg : :obj:`str`
			Message to be recieved by eyelink.

		Examples
		--------
		>>> eyetracking.console(msg="eyetracking.calibration() started", color="blue")
		"""
		self.tracker.sendMessage(msg)

	def send_variable(self, variables):
		"""
		send trial variable to eyelink at the end of trial.

		Parameters
		----------
		variable : :obj:`dict` or :obj:`None`
			Trial-related variables to be read by eyelink.          
		"""
		if variables is not None:
			for key, value in variables.items():
				msg = "!V TRIAL_VAR %s %s" % (key, value)
				self.tracker.sendMessage(msg)
				pylink.msecDelay(1)
			# finished
			self.console("variables sent")
		else:
			self.console("no variables entered", "red")

	def stop_recording(self, trial=None, block=None, variables=None):
		"""
		Stops recording of Eyelink. Also allows transmission of trial-level variables to Eyelink.

		Parameters
		----------
		trial : :obj:`int`
			Trial Number.
		block : :obj:`int`
			Block Number.
		variables : :obj:`dict` or :obj:`None`
			Dict of variables to send to eyelink (variable name, value).

		Returns
		-------
		isRecording : :obj:`bool`
			Message indicating status of Eyelink recording.

		Notes
		-----
		pylink.pumpDelay():
			Does a unblocked delay using currentTime(). This is the preferred delay function 
			when accurate timing is not needed.
			[see `pylink.chm <../../pylink.chm>`_]
		pylink.msecDelay():
			During calls to pylink.msecDelay(), Windows is not able to handle messages. One result of 
			this is that windows may not appear. This is the preferred delay function when accurate 
			timing is needed.
			[see `pylink.chm <../../pylink.chm>`_]
		tracker.endRealTimeMode():
			Returns the application to a priority slightly above normal, to end realtime mode. This 
			function should execute rapidly, but there is the possibility that Windows will allow other 
			tasks to run after this call, causing delays of 1-20 milliseconds. This function is equivalent 
			to the C API void end_realtime_mode(void).
			[see `pylink.chm <../../pylink.chm>`_]
		TRIAL_VAR: 
			Lets users specify a trial variable and value for the given trial. One
			message should be sent for each trial condition variable and its corresponding value. If
			this command is used there is no need to use TRIAL_VAR_LABELS. The default command identifier 
			can be changed in the data loading preference settings. Please note that the eye tracker 
			can handle about 20 messages every 10 milliseconds. So be careful not to send too many 
			messages too quickly if you have many trial condition messages to send. Add one millisecond 
			delay between message lines if this is the case.
			[see `pylink.chm <../../pylink.chm>`_]
		TRIAL_RESULT: 
			Defines the end of a trial. The viewer will not parse any messages, events, or samples that 
			exist in the data file after this message. The command identifier can be changed in the 
			data loading preference settings.
			[see `Data Viewer User Manual, Section 7: Protocol for EyeLink Data to Viewer Integration \
			<../manual/EyeLink%20Data%20Viewer%20User%20Manual%203.2.1.pdf>`_]

		Examples
		--------
		>>> variables = dict(stimulus='face.png', event='stimulus')
		>>> eyetracking.stop_recording(trial=trial, block=block, variables=variables)
		"""
		self.console("eyetracking.stop_recording()")

		# end of trial message
		self.tracker.sendMessage('end recording')

		# end realtime mode
		pylink.endRealTimeMode()
		pylink.msecDelay(100)

		# send trial-level variables
		variables['trial'] = trial
		variables['block'] = block
		variables['issues'] = 'none'
		self.send_variable(variables=variables)

		# specify end of trial
		self.tracker.sendMessage("TRIAL_RESULT 1")

		# stop recording eye data
		self.tracker.stopRecording()

		# flag isRecording
		self.isRecording = False

		return self.isRecording

	def finish_recording(self, path=None):
		"""
		Ending Eyelink recording.

		Parameters
		----------
		path : :obj:`str` or :obj:`None`
			Path to save data. If None, path will be default from PsychoPy task.

		Returns
		-------
		isFinished : :obj:`bool`
			Message indicating status of Eyelink recording.

		Notes
		-----
		pylink.pumpDelay():
			Does a unblocked delay using currentTime(). This is the preferred delay function 
			when accurate timing is not needed.
			[see `pylink.chm <../manual/pylink.chm>`_]
		pylink.msecDelay():
			During calls to pylink.msecDelay(), Windows is not able to handle messages. One result of 
			this is that windows may not appear. This is the preferred delay function when accurate 
			timing is needed.
			[see `pylink.chm <../manual/pylink.chm>`_]
		tracker.setOfflineMode():
			Places EyeLink tracker in offline (idle) mode. Wait till the tracker has finished the 
			mode transition.
			[see `pylink.chm <../manual/pylink.chm>`_]
		tracker.endRealTimeMode():
			Returns the application to a priority slightly above normal, to end realtime mode. This 
			function should execute rapidly, but there is the possibility that Windows will allow other 
			tasks to run after this call, causing delays of 1-20 milliseconds. This function is 
			equivalent to the C API void end_realtime_mode(void).
			[see `pylink.chm <../manual/pylink.chm>`_]
		tracker.receiveDataFile():
			This receives a data file from the EyeLink tracker PC. Source filename and destination 
			filename should be given.
			[see `pylink.chm <../manual/pylink.chm>`_]

		Examples
		--------
		>>> #end recording session
		>>> eyetracking.finish_recording()
		"""
		self.console("eyetracking.finish_recording()")

		# clear host display
		self.tracker.sendCommand('clear_screen 0') 

		# double check realtime mode has ended
		pylink.endRealTimeMode()
		pylink.pumpDelay(500)

		# rlaces eyeLink tracker in offline (idle) mode
		self.tracker.setOfflineMode()

		# allow buffer to prepare data for closing
		pylink.msecDelay(500)

		# closes any currently opened EDF file on the EyeLink tracker computer's hard disk
		self.tracker.closeDataFile()

		# This receives a data file from the eyelink tracking computer
		#if no path entered, use psychopy destination
		if path is None:
			destination = self.path + self.fname
		else:
			destination = path
		self.tracker.receiveDataFile(self.fname, destination)
		self.console("File saved at: %s"%(Path(destination)), "blue")

		# sends a disconnect message to the EyeLink tracker
		self.tracker.close()

		# flag isFinished
		self.isFinished = True

		#---finish
		return self.isFinished
