#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import absolute_import, print_function

from builtins import str
from builtins import object
import os
from pathlib import Path
from xml.etree.ElementTree import Element
import re
import wx.__version__
import psychopy
from psychopy import logging
from psychopy.experiment.components import Param, _translate
from psychopy.experiment.routines.eyetracker_calibrate import EyetrackerCalibrationRoutine
import psychopy.tools.versionchooser as versions
from psychopy.constants import PY3
from psychopy.monitors import Monitor
from psychopy.iohub import util as ioUtil
from psychopy.alerts import alert

# for creating html output folders:
import shutil
import hashlib
from pkg_resources import parse_version
import ast  # for doing literal eval to convert '["a","b"]' to a list


def readTextFile(relPath):
    fullPath = os.path.join(Path(__file__).parent, relPath)
    with open(fullPath, "r") as f:
        txt = f.read()
    return txt


# used when writing scripts and in namespace:
_numpyImports = ['sin', 'cos', 'tan', 'log', 'log10', 'pi', 'average',
                 'sqrt', 'std', 'deg2rad', 'rad2deg', 'linspace', 'asarray']
_numpyRandomImports = ['random', 'randint', 'normal', 'shuffle', 'choice as randchoice']

# this is not a standard component - it will appear on toolbar not in
# components panel

# only use _localized values for label values, nothing functional:
_localized = {'expName': _translate("Experiment name"),
              'Show info dlg':  _translate("Show info dialog"),
              'Enable Escape':  _translate("Enable Escape key"),
              'Experiment info':  _translate("Experiment info"),
              'Data filename':  _translate("Data filename"),
              'Data file delimiter':  _translate("Data file delimiter"),
              'Full-screen window':  _translate("Full-screen window"),
              'Window size (pixels)':  _translate("Window size (pixels)"),
              'Screen': _translate('Screen'),
              'Monitor':  _translate("Monitor"),
              'color': _translate("Color"),
              'colorSpace':  _translate("Color space"),
              'Units':  _translate("Units"),
              'blendMode':   _translate("Blend mode"),
              'Show mouse':  _translate("Show mouse"),
              'Save log file':  _translate("Save log file"),
              'Save wide csv file':
                  _translate("Save csv file (trial-by-trial)"),
              'Save csv file': _translate("Save csv file (summaries)"),
              'Save excel file':  _translate("Save excel file"),
              'Save psydat file':  _translate("Save psydat file"),
              'logging level': _translate("Logging level"),
              'Use version': _translate("Use PsychoPy version"),
              'Completed URL': _translate("Completed URL"),
              'Incomplete URL': _translate("Incomplete URL"),
              'Output path': _translate("Output path"),
              'Additional Resources': _translate("Additional Resources"),
              'JS libs': _translate("JS libs"),
              'Force stereo': _translate("Force stereo"),
              'Export HTML': _translate("Export HTML")}
ioDeviceMap = dict(ioUtil.getDeviceNames())
#
#
# # customize the Proj ID Param class to
# class ProjIDParam(Param):
#     @property
#     def allowedVals(self):
#         from psychopy.app.projects import catalog
#         allowed = list(catalog.keys())
#         # always allow the current val!
#         if self.val not in allowed:
#             allowed.append(self.val)
#         # always allow blank (None)
#         if '' not in allowed:
#             allowed.append('')
#         return allowed
#     @allowedVals.setter
#     def allowedVals(self, allowed):
#         pass


class SettingsComponent(object):
    """This component stores general info about how to run the experiment
    """
    targets = ['PsychoPy']

    categories = ['Custom']
    targets = ['PsychoPy', 'PsychoJS']
    iconFile = Path(__file__).parent / 'settings.png'
    tooltip = _translate("Edit settings for this experiment")

    def __init__(self, parentName, exp, expName='', fullScr=True,
                 winSize=(1024, 768), screen=1, monitor='testMonitor',
                 showMouse=False, saveLogFile=True, showExpInfo=True,
                 expInfo="{'participant':'', 'session':'001'}",
                 units='height', logging='exp',
                 color='$[0,0,0]', colorSpace='rgb', enableEscape=True,
                 blendMode='avg',
                 saveXLSXFile=False, saveCSVFile=False, saveHDF5File=False,
                 saveWideCSVFile=True, savePsydatFile=True,
                 savedDataFolder='', savedDataDelim='auto',
                 useVersion='',
                 eyetracker="None",
                 mgMove='CONTINUOUS', mgBlink='MIDDLE_BUTTON', mgSaccade=0.5,
                 gpAddress='127.0.0.1', gpPort=4242,
                 elModel='EYELINK 1000 DESKTOP', elSimMode=False, elSampleRate=1000, elTrackEyes="RIGHT_EYE",
                 elLiveFiltering="FILTER_LEVEL_OFF", elDataFiltering="FILTER_LEVEL_2",
                 elTrackingMode='PUPIL_CR_TRACKING', elPupilMeasure='PUPIL_AREA', elPupilAlgorithm='ELLIPSE_FIT',
                 elAddress='100.1.1.1',
                 tbModel="", tbLicenseFile="", tbSerialNo="", tbSampleRate=60,
                 filename=None, exportHTML='on Sync'):
        self.type = 'Settings'
        self.exp = exp  # so we can access the experiment if necess
        self.exp.requirePsychopyLibs(['visual', 'gui'])
        self.parentName = parentName
        self.url = "https://www.psychopy.org/builder/settings.html"
        self._monitor = None

        # if filename is the default value fetch the builder pref for the
        # folder instead
        if filename is None:
            filename = ("u'xxxx/%s_%s_%s' % (expInfo['participant'], expName,"
                        " expInfo['date'])")
        if filename.startswith("u'xxxx"):
            folder = self.exp.prefsBuilder['savedDataFolder'].strip()
            filename = filename.replace("xxxx", folder)

        # params
        self.params = {}
        self.depends = []
        self.order = ['expName', 'Use version', 'Show info dlg', 'Enable Escape',  'Experiment info',  # Basic tab
                      'Data filename', 'Data file delimiter', 'Save excel file', 'Save csv file', 'Save wide csv file',
                      'Save psydat file', 'Save hdf5 file', 'Save log file', 'logging level',  # Data tab
                      'Audio lib', 'Audio latency priority', "Force stereo",  # Audio tab
                      'HTML path', 'exportHTML', 'Completed URL', 'Incomplete URL', 'Resources',  # Online tab
                      'Monitor', 'Screen', 'Full-screen window', 'Window size (pixels)', 'Units', 'color',
                      'colorSpace',  # Screen tab
                      ]
        self.depends = []
        # basic params
        self.params['expName'] = Param(
            expName, valType='str',  inputType="single", allowedTypes=[],
            hint=_translate("Name of the entire experiment (taken by default"
                            " from the filename on save)"),
            label=_localized["expName"])
        self.params['Show info dlg'] = Param(
            showExpInfo, valType='bool', inputType="bool", allowedTypes=[],
            hint=_translate("Start the experiment with a dialog to set info"
                            " (e.g.participant or condition)"),
            label=_localized["Show info dlg"], categ='Basic')
        self.params['Enable Escape'] = Param(
            enableEscape, valType='bool', inputType="bool", allowedTypes=[],
            hint=_translate("Enable the <esc> key, to allow subjects to quit"
                            " / break out of the experiment"),
            label=_localized["Enable Escape"])
        self.params['Experiment info'] = Param(
            expInfo, valType='code', inputType="dict", allowedTypes=[],
            hint=_translate("The info to present in a dialog box. Right-click"
                            " to check syntax and preview the dialog box."),
            label=_localized["Experiment info"], categ='Basic')
        self.params['Use version'] = Param(
            useVersion, valType='str', inputType="choice",
            # search for options locally only by default, otherwise sluggish
            allowedVals=versions._versionFilter(versions.versionOptions(), wx.__version__)
                        + ['']
                        + versions._versionFilter(versions.availableVersions(), wx.__version__),
            hint=_translate("The version of PsychoPy to use when running "
                            "the experiment."),
            label=_localized["Use version"], categ='Basic')

        # screen params
        self.params['Full-screen window'] = Param(
            fullScr, valType='bool', inputType="bool", allowedTypes=[],
            hint=_translate("Run the experiment full-screen (recommended)"),
            label=_localized["Full-screen window"], categ='Screen')
        self.params['Window size (pixels)'] = Param(
            winSize, valType='list', inputType="single", allowedTypes=[],
            hint=_translate("Size of window (if not fullscreen)"),
            label=_localized["Window size (pixels)"], categ='Screen')
        self.params['Screen'] = Param(
            screen, valType='num', inputType="spin", allowedTypes=[],
            hint=_translate("Which physical screen to run on (1 or 2)"),
            label=_localized["Screen"], categ='Screen')
        self.params['Monitor'] = Param(
            monitor, valType='str', inputType="single", allowedTypes=[],
            hint=_translate("Name of the monitor (from Monitor Center). Right"
                            "-click to go there, then copy & paste a monitor "
                            "name here."),
            label=_localized["Monitor"], categ="Screen")
        self.params['color'] = Param(
            color, valType='color', inputType="color", allowedTypes=[],
            hint=_translate("Color of the screen (e.g. black, $[1.0,1.0,1.0],"
                            " $variable. Right-click to bring up a "
                            "color-picker.)"),
            label=_localized["color"], categ='Screen')
        self.params['colorSpace'] = Param(
            colorSpace, valType='str', inputType="choice",
            hint=_translate("Needed if color is defined numerically (see "
                            "PsychoPy documentation on color spaces)"),
            allowedVals=['rgb', 'dkl', 'lms', 'hsv', 'hex'],
            label=_localized["colorSpace"], categ="Screen")
        self.params['Units'] = Param(
            units, valType='str', inputType="choice", allowedTypes=[],
            allowedVals=['use prefs', 'deg', 'pix', 'cm', 'norm', 'height',
                         'degFlatPos', 'degFlat'],
            hint=_translate("Units to use for window/stimulus coordinates "
                            "(e.g. cm, pix, deg)"),
            label=_localized["Units"], categ='Screen')
        self.params['blendMode'] = Param(
            blendMode, valType='str', inputType="choice",
            allowedTypes=[], allowedVals=['add', 'avg'],
            hint=_translate("Should new stimuli be added or averaged with "
                            "the stimuli that have been drawn already"),
            label=_localized["blendMode"], categ='Screen')
        self.params['Show mouse'] = Param(
            showMouse, valType='bool', inputType="bool", allowedTypes=[],
            hint=_translate("Should the mouse be visible on screen?"),
            label=_localized["Show mouse"], categ='Screen')

        # sound params
        self.params['Force stereo'] = Param(
            enableEscape, valType='bool', inputType="bool", allowedTypes=[], categ="Audio",
            hint=_translate("Force audio to stereo (2-channel) output"),
            label=_localized["Force stereo"])
        self.params['Audio lib'] = Param(
            'use prefs', valType='str', inputType="choice",
            allowedVals=['use prefs', 'ptb', 'pyo', 'sounddevice', 'pygame'],
            hint=_translate("Which Python sound engine do you want to play your sounds?"),
            label=_translate("Audio library"), categ='Audio')

        audioLatencyLabels = [
            _translate('use prefs'),
            '0: ' + _translate('Latency not important'),
            '1: ' + _translate('Share low-latency driver'),
            '2: ' + _translate('Exclusive low-latency'),
            '3: ' + _translate('Aggressive low-latency'),
            '4: ' + _translate('Latency critical'),
        ]
        self.params['Audio latency priority'] = Param(
            'use prefs', valType='str', inputType="choice",
            allowedVals=['use prefs', '0', '1', '2', '3', '4'],
            allowedLabels=audioLatencyLabels,
            hint=_translate("How important is audio latency for you? If essential then you may need to get all your sounds in correct formats."),
            label=_translate("Audio latency priority"), categ='Audio')

        # data params
        self.params['Data filename'] = Param(
            filename, valType='code', inputType="single", allowedTypes=[],
            hint=_translate("Code to create your custom file name base. Don"
                            "'t give a file extension - this will be added."),
            label=_localized["Data filename"], categ='Data')
        self.params['Data file delimiter'] = Param(
            savedDataDelim, valType='str', inputType="choice",
            allowedVals=['auto', 'comma', 'semicolon', 'tab'],
            hint=_translate("What symbol should the data file use to separate columns? ""Auto"" will select a delimiter automatically from the filename."),
            label=_translate("Data file delimiter"), categ='Data'
        )
        self.params['Save log file'] = Param(
            saveLogFile, valType='bool', inputType="bool", allowedTypes=[],
            hint=_translate("Save a detailed log (more detailed than the "
                            "excel/csv files) of the entire experiment"),
            label=_localized["Save log file"], categ='Data')
        self.params['Save wide csv file'] = Param(
            saveWideCSVFile, valType='bool', inputType="bool", allowedTypes=[],
            hint=_translate("Save data from loops in comma-separated-value "
                            "(.csv) format for maximum portability"),
            label=_localized["Save wide csv file"], categ='Data')
        self.params['Save csv file'] = Param(
            saveCSVFile, valType='bool', inputType="bool", allowedTypes=[],
            hint=_translate("Save data from loops in comma-separated-value "
                            "(.csv) format for maximum portability"),
            label=_localized["Save csv file"], categ='Data')
        self.params['Save excel file'] = Param(
            saveXLSXFile, valType='bool', inputType="bool", allowedTypes=[],
            hint=_translate("Save data from loops in Excel (.xlsx) format"),
            label=_localized["Save excel file"], categ='Data')
        self.params['Save psydat file'] = Param(
            savePsydatFile, valType='bool', inputType="bool", allowedVals=[True],
            hint=_translate("Save data from loops in psydat format. This is "
                            "useful for python programmers to generate "
                            "analysis scripts."),
            label=_localized["Save psydat file"], categ='Data')
        self.params['Save hdf5 file'] = Param(
            saveHDF5File, valType='bool', inputType="bool",
            hint=_translate("Save data from eyetrackers in hdf5 format. This is "
                            "useful for viewing and analyzing complex data in structures."),
            label=_translate("Save hdf5 file"), categ='Data')
        self.params['logging level'] = Param(
            logging, valType='code', inputType="choice",
            allowedVals=['error', 'warning', 'data', 'exp', 'info', 'debug'],
            hint=_translate("How much output do you want in the log files? "
                            "('error' is fewest messages, 'debug' is most)"),
            label=_localized["logging level"], categ='Data')

        # HTML output params
        # self.params['OSF Project ID'] = ProjIDParam(
        #     '', valType='str', # automatically updates to allow choices
        #     hint=_translate("The ID of this project (e.g. 5bqpc)"),
        #     label="OSF Project ID", categ='Online')
        self.params['HTML path'] = Param(
            '', valType='str', inputType="single", allowedTypes=[],
            hint=_translate("Place the HTML files will be saved locally "),
            label="Output path", categ='Online')
        self.params['Resources'] = Param(
            [], valType='list', inputType="fileList", allowedTypes=[],
            hint=_translate("Any additional resources needed"),
            label="Additional Resources", categ='Online')
        self.params['Completed URL'] = Param(
            '', valType='str', inputType="single",
            hint=_translate("Where should participants be redirected after the experiment on completion\n"
                            " INSERT COMPLETION URL E.G.?"),
            label="Completed URL", categ='Online')
        self.params['Incomplete URL'] = Param(
            '', valType='str', inputType="single",
            hint=_translate("Where participants are redirected if they do not complete the task\n"
                            " INSERT INCOMPLETION URL E.G.?"),
            label="Incomplete URL", categ='Online')
        self.params['exportHTML'] = Param(
            exportHTML, valType='str', inputType="choice",
            allowedVals=['on Save', 'on Sync', 'manually'],
            hint=_translate("When to export experiment to the HTML folder."),
            label=_localized["Export HTML"], categ='Online')

        # Eyetracking params
        self.order += ["eyetracker",
                       "gpAddress", "gpPort",
                       "elModel", "elAddress", "elSimMode"]

        # Hide params when not relevant to current eyetracker
        trackerParams = {
            "MouseGaze": ["mgMove", "mgBlink", "mgSaccade"],
            "GazePoint": ["gpAddress", "gpPort"],
            "SR Research Ltd": ["elModel", "elSimMode", "elSampleRate", "elTrackEyes", "elLiveFiltering",
                                "elDataFiltering", "elTrackingMode", "elPupilMeasure", "elPupilAlgorithm",
                                "elAddress"],
            "Tobii Technology": ["tbModel", "tbLicenseFile", "tbSerialNo", "tbSampleRate"],
        }
        for tracker in trackerParams:
            for depParam in trackerParams[tracker]:
                self.depends.append(
                    {"dependsOn": "eyetracker",  # must be param name
                     "condition": "=='"+tracker+"'",  # val to check for
                     "param": depParam,  # param property to alter
                     "true": "show",  # what to do with param if condition is True
                     "false": "hide",  # permitted: hide, show, enable, disable
                     }
                )
        self.depends.append(
            {"dependsOn": "eyetracker",  # must be param name
             "condition": f" in {list(trackerParams)}",  # val to check for
             "param": "Save hdf5 file",  # param property to alter
             "true": "enable",  # what to do with param if condition is True
             "false": "disable",  # permitted: hide, show, enable, disable
             }
        )

        self.params['eyetracker'] = Param(
            eyetracker, valType='str', inputType="choice",
            allowedVals=list(ioDeviceMap) + ["None"],
            hint=_translate("What kind of eye tracker should PsychoPy use? Select 'MouseGaze' to use "
                            "the mouse to simulate eye movement (for debugging without a tracker connected)"),
            label=_translate("Eyetracker Device"), categ="Eyetracking"
        )

        #mousegaze
        self.params['mgMove'] = Param(
            mgMove, valType='str', inputType="choice",
            allowedVals=['CONTINUOUS', 'LEFT_BUTTON', 'MIDDLE_BUTTON', 'RIGHT_BUTTON'],
            hint=_translate("Mouse button to press for eye movement."),
            label=_translate("Move Button"), categ="Eyetracking"
        )

        self.params['mgBlink'] = Param(
            mgBlink, valType='list', inputType="multiChoice",
            allowedVals=['LEFT_BUTTON', 'MIDDLE_BUTTON', 'RIGHT_BUTTON'],
            hint=_translate("Mouse button to press for a blink."),
            label=_translate("Blink Button"), categ="Eyetracking"
        )

        self.params['mgSaccade'] = Param(
            mgSaccade, valType='num', inputType="single",
            hint=_translate("Visual degree threshold for Saccade event creation."),
            label=_translate("Saccade Threshold"), categ="Eyetracking"
        )

        # gazepoint
        self.params['gpAddress'] = Param(
            gpAddress, valType='str', inputType="single",
            hint=_translate("IP Address of the computer running GazePoint Control."),
            label=_translate("GazePoint IP Address"), categ="Eyetracking"
        )

        self.params['gpPort'] = Param(
            gpPort, valType='num', inputType="single",
            hint=_translate("Port of the GazePoint Control server. Usually 4242."),
            label=_translate("GazePoint Port"), categ="Eyetracking"
        )
        # eyelink
        self.params['elModel'] = Param(
            elModel, valType='str', inputType="choice",
            allowedVals=['EYELINK 1000 DESKTOP', 'EYELINK 1000 TOWER', 'EYELINK 1000 REMOTE',
                         'EYELINK 1000 LONG RANGE'],
            hint=_translate("Eye tracker model."),
            label=_translate("Model Name"), categ="Eyetracking"
        )

        self.params['elSimMode'] = Param(
            elSimMode, valType='bool', inputType="bool",
            hint=_translate("Set the EyeLink to run in mouse simulation mode."),
            label=_translate("Mouse Simulation Mode"), categ="Eyetracking"
        )

        self.params['elSampleRate'] = Param(
            elSampleRate, valType='num', inputType="choice",
            allowedVals=['250', '500', '1000', '2000'],
            hint=_translate("Eye tracker sampling rate."),
            label=_translate("Sampling Rate"), categ="Eyetracking"
        )

        self.params['elTrackEyes'] = Param(
            elTrackEyes, valType='str', inputType="choice",
            allowedVals=['LEFT_EYE', 'RIGHT_EYE', 'BOTH'],
            hint=_translate("Select with eye(s) to track."),
            label=_translate("Track Eyes"), categ="Eyetracking"
        )

        self.params['elLiveFiltering'] = Param(
            elLiveFiltering, valType='str', inputType="choice",
            allowedVals=['FILTER_LEVEL_OFF', 'FILTER_LEVEL_1', 'FILTER_LEVEL_2'],
            hint=_translate("Filter eye sample data live, as it is streamed to the driving device. "
                            "This may reduce the sampling speed."),
            label=_translate("Live Sample Filtering"), categ="Eyetracking"
        )

        self.params['elDataFiltering'] = Param(
            elDataFiltering, valType='str', inputType="choice",
            allowedVals=['FILTER_LEVEL_OFF', 'FILTER_LEVEL_1', 'FILTER_LEVEL_2'],
            hint=_translate("Filter eye sample data when it is saved to the output file. This will "
                            "not affect the sampling speed."),
            label=_translate("Saved Sample Filtering"), categ="Eyetracking"
        )

        self.params['elTrackingMode'] = Param(
            elTrackingMode, valType='str', inputType="choice",
            allowedVals=['PUPIL_CR_TRACKING', 'PUPIL_ONLY_TRACKING'],
            hint=_translate("Track Pupil-CR or Pupil only."),
            label=_translate("Pupil Tracking Mode"), categ="Eyetracking"
        )

        self.params['elPupilAlgorithm'] = Param(
            elPupilAlgorithm, valType='str', inputType="choice",
            allowedVals=['ELLIPSE_FIT', 'CENTROID_FIT'],
            hint=_translate("Algorithm used to detect the pupil center."),
            label=_translate("Pupil Center Algorithm"), categ="Eyetracking"
        )

        self.params['elPupilMeasure'] = Param(
            elPupilMeasure, valType='str', inputType="choice",
            allowedVals=['PUPIL_AREA', 'PUPIL_DIAMETER', 'NEITHER'],
            hint=_translate("Type of pupil data to record."),
            label=_translate("Pupil Data Type"), categ="Eyetracking"
        )

        self.params['elAddress'] = Param(
            elAddress, valType='str', inputType="single",
            hint=_translate("IP Address of the EyeLink *Host* computer."),
            label=_translate("EyeLink IP Address"), categ="Eyetracking"
        )

        # tobii
        self.params['tbModel'] = Param(
            tbModel, valType='str', inputType="single",
            hint=_translate("Eye tracker model."),
            label=_translate("Model Name"), categ="Eyetracking"
        )

        self.params['tbLicenseFile'] = Param(
            tbLicenseFile, valType='str', inputType="file",
            hint=_translate("Eye tracker license file (optional)."),
            label=_translate("License File"), categ="Eyetracking"
        )

        self.params['tbSerialNo'] = Param(
            tbSerialNo, valType='str', inputType="single",
            hint=_translate("Eye tracker serial number (optional)."),
            label=_translate("Serial Number"), categ="Eyetracking"
        )

        self.params['tbSampleRate'] = Param(
            tbSampleRate, valType='num', inputType="single",
            hint=_translate("Eye tracker sampling rate."),
            label=_translate("Sampling Rate"), categ="Eyetracking"
        )

    @property
    def xml(self):
        # Make root element
        element = Element("Settings")
        # Add an element for each parameter
        for key, param in sorted(self.params.items()):
            if key == 'name':
                continue
            # Create node
            paramNode = param.xml
            paramNode.set("name", key)
            # Add node
            element.append(paramNode)
        return element

    def getInfo(self):
        """Rather than converting the value of params['Experiment Info']
        into a dict from a string (which can lead to errors) use this function
        :return: expInfo as a dict
        """
        
        infoStr = self.params['Experiment info'].val.strip()
        if len(infoStr) == 0:
            return {}
        try:
            infoDict = ast.literal_eval(infoStr)
            # check for strings of lists: "['male','female']"
            for key in infoDict:
                val = infoDict[key]
                if (hasattr(val, 'startswith')
                        and val.startswith('[') and val.endswith(']')):
                    try:
                        infoDict[key] = ast.literal_eval(val)
                    except (ValueError, SyntaxError):
                        logging.warning("Tried and failed to parse {!r}"
                                        "as a list of values."
                                        .format(val))
                elif val in ['True', 'False']:
                    infoDict[key] = ast.literal_eval(val)

        except (ValueError, SyntaxError):
            """under Python3 {'participant':'', 'session':02} raises an error because 
            ints can't have leading zeros. We will check for those and correct them
            tests = ["{'participant':'', 'session':02}",
                    "{'participant':'', 'session':02}",
                    "{'participant':'', 'session': 0043}",
                    "{'participant':'', 'session':02, 'id':009}",
                    ]
                    """

            def entryToString(match):
                entry = match.group(0)
                digits = re.split(r": *", entry)[1]
                return ':{}'.format(repr(digits))

            # 0 or more spaces, 1-5 zeros, 0 or more digits:
            pattern = re.compile(r": *0{1,5}\d*")
            try:
                infoDict = eval(re.sub(pattern, entryToString, infoStr))
            except SyntaxError:  # still a syntax error, possibly caused by user
                msg = ('Builder Expt: syntax error in '
                              '"Experiment info" settings (expected a dict)')
                logging.error(msg)
                raise AttributeError(msg)
        return infoDict

    def getType(self):
        return self.__class__.__name__

    def getShortType(self):
        return self.getType().replace('Component', '')

    def getSaveDataDir(self):
        if 'Saved data folder' in self.params:
            # we have a param for the folder (deprecated since 1.80)
            saveToDir = self.params['Saved data folder'].val.strip()
            if not saveToDir:  # it was blank so try preferences
                saveToDir = self.exp.prefsBuilder['savedDataFolder'].strip()
        else:
            saveToDir = os.path.dirname(self.params['Data filename'].val)
        return saveToDir or u'data'

    def writeUseVersion(self, buff):
        if self.params['Use version'].val:
            code = ('\nimport psychopy\n'
                    'psychopy.useVersion({})\n\n')
            val = repr(self.params['Use version'].val)
            buff.writeIndentedLines(code.format(val))

    def writeInitCode(self, buff, version, localDateTime):

        buff.write(
            '#!/usr/bin/env python\n'
            '# -*- coding: utf-8 -*-\n'
            '"""\nThis experiment was created using PsychoPy3 Experiment '
            'Builder (v%s),\n'
            '    on %s\n' % (version, localDateTime) +
            'If you publish work using this script the most relevant '
            'publication is:\n\n'            
            u'    Peirce J, Gray JR, Simpson S, MacAskill M, Höchenberger R, Sogo H, '
            u'Kastman E, Lindeløv JK. (2019) \n'
            '        PsychoPy2: Experiments in behavior made easy Behav Res 51: 195. \n'
            '        https://doi.org/10.3758/s13428-018-01193-y\n'
            '\n"""\n'
            "\nfrom __future__ import absolute_import, division\n")

        self.writeUseVersion(buff)

        psychopyImports = []
        customImports = []
        for import_ in self.exp.requiredImports:
            if import_.importFrom == 'psychopy':
                psychopyImports.append(import_.importName)
            else:
                customImports.append(import_)

        buff.writelines(
            "\nfrom psychopy import locale_setup\n"
            "from psychopy import prefs\n"
        )
        # adjust the prefs for this study if needed
        if self.params['Audio lib'].val.lower() != 'use prefs':
            buff.writelines(
                "prefs.hardware['audioLib'] = {}\n".format(self.params['Audio lib'])
            )
        if self.params['Audio latency priority'].val.lower() != 'use prefs':
            buff.writelines(
                "prefs.hardware['audioLatencyMode'] = {}\n".format(self.params['Audio latency priority'])
            )
        buff.write(
            "from psychopy import %s\n" % ', '.join(psychopyImports) +
            "from psychopy.constants import (NOT_STARTED, STARTED, PLAYING,"
            " PAUSED,\n"
            "                                STOPPED, FINISHED, PRESSED, "
            "RELEASED, FOREVER)\n\n"
            "import numpy as np  # whole numpy lib is available, "
            "prepend 'np.'\n"
            "from numpy import (%s,\n" % ', '.join(_numpyImports[:7]) +
            "                   %s)\n" % ', '.join(_numpyImports[7:]) +
            "from numpy.random import %s\n" % ', '.join(_numpyRandomImports) +
            "import os  # handy system and path functions\n" +
            "import sys  # to get file system encoding\n"
            "\n")

        if not self.params['eyetracker'] == "None":
            code = (
                "import psychopy.iohub as io\n"
            )
            buff.writeIndentedLines(code)

        # Write custom import statements, line by line.
        for import_ in customImports:
            importName = import_.importName
            importFrom = import_.importFrom
            importAs = import_.importAs

            statement = ''
            if importFrom:
                statement += "from %s " % importFrom

            statement += "import %s" % importName

            if importAs:
                statement += " as %s" % importAs

            statement += "\n"
            buff.write(statement)

        buff.write("\n")


    def prepareResourcesJS(self):
        """Sets up the resources folder and writes the info.php file for PsychoJS
        """

        join = os.path.join

        def copyTreeWithMD5(src, dst):
            """Copies the tree but checks SHA for each file first
            """
            # despite time to check the md5 hashes this func gives speed-up
            # over about 20% over using shutil.rmtree() and copytree()
            for root, subDirs, files in os.walk(src):
                relPath = os.path.relpath(root, src)
                for thisDir in subDirs:
                    if not os.path.isdir(join(root, thisDir)):
                        os.makedirs(join(root, thisDir))
                for thisFile in files:
                    copyFileWithMD5(join(root, thisFile),
                                    join(dst, relPath, thisFile))

        def copyFileWithMD5(src, dst):
            """Copies a file but only if doesn't exist or SHA is diff
            """
            if os.path.isfile(dst):
                with open(dst, 'rb') as f:
                    dstMD5 = hashlib.md5(f.read()).hexdigest()
                with open(src, 'rb') as f:
                    srcMD5 = hashlib.md5(f.read()).hexdigest()
                if srcMD5 == dstMD5:
                    return  # already matches - do nothing
                # if we got here then the file exists but not the same
                # delete and replace. TODO: In future this should check date
                os.remove(dst)
            # either didn't exist or has been deleted
            folder = os.path.split(dst)[0]
            if not os.path.isdir(folder):
                os.makedirs(folder)
            shutil.copy2(src, dst)

        # write info.php file
        folder = os.path.dirname(self.exp.expPath)
        if not os.path.isdir(folder):
            os.mkdir(folder)

        # is email a defined parameter for this version
        if 'email' in self.params:
            email = repr(self.params['email'].val)
        else:
            email = "''"
        # populate resources folder
        resFolder = join(folder, 'resources')
        if not os.path.isdir(resFolder):
            os.mkdir(resFolder)
        resourceFiles = self.exp.getResourceFiles()

        for srcFile in resourceFiles:
            dstAbs = os.path.normpath(join(resFolder, srcFile['rel']))
            dstFolder = os.path.split(dstAbs)[0]
            if not os.path.isdir(dstFolder):
                os.makedirs(dstFolder)
            copyFileWithMD5(srcFile['abs'], dstAbs)

    def writeInitCodeJS(self, buff, version, localDateTime, modular=True):
        # create resources folder
        if self.exp.htmlFolder:
            self.prepareResourcesJS()
        jsFilename = os.path.basename(os.path.splitext(self.exp.filename)[0])

        # decide if we need anchored useVersion or leave plain
        useVer = self.params['Use version'].val
        if useVer == '':
            useVer = version
        elif useVer == 'latest':
            useVer = versions.latestVersion()
        else:
            version = versions.fullVersion(useVer)

        # do we shorten minor versions ('3.4.2' to '3.4')?
        # only from 3.2 onwards
        if (parse_version('3.2')) <= parse_version(useVer) < parse_version('2021') \
                and len(useVer.split('.')) > 2:
            # e.g. 2020.2 not 2021.2.5
            useVer = '.'.join(useVer.split('.')[:2])
        elif len(useVer.split('.')) > 3:
            # e.g. 2021.1.0 not 2021.1.0.dev3
            useVer = '.'.join(useVer.split('.')[:3])

        # prepend the hyphen
        versionStr = '-{}'.format(useVer)

        # html header
        template = readTextFile("JS_htmlHeader.tmpl")
        header = template.format(
            name=jsFilename,
            version=versionStr,
            params=self.params)
        jsFile = self.exp.expPath
        folder = os.path.dirname(jsFile)
        if not os.path.isdir(folder):
            os.makedirs(folder)
        with open(os.path.join(folder, "index.html"), 'wb') as html:
            html.write(header.encode())
        html.close()

        # Write header comment
        starLen = "*"*(len(jsFilename) + 9)
        code = ("/%s \n"
               " * %s Test *\n" 
               " %s/\n\n")
        buff.writeIndentedLines(code % (starLen, jsFilename.title(), starLen))

        # Write imports if modular
        if modular:
            code = (
                    "import {{ core, data, sound, util, visual }} from './lib/psychojs-2021.2.0.js';\n"
                    "const {{ PsychoJS }} = core;\n"
                    "const {{ TrialHandler }} = data;\n"
                    "const {{ Scheduler }} = util;\n"
                    "//some handy aliases as in the psychopy scripts;\n"
                    "const {{ abs, sin, cos, PI: pi, sqrt }} = Math;\n"
                    "const {{ round }} = util;\n"
                    "\n").format(version=versionStr)
            buff.writeIndentedLines(code)

        code = ("\n// store info about the experiment session:\n"
                "let expName = '%s';  // from the Builder filename that created this script\n"
                "let expInfo = %s;\n"
                "\n" % (jsFilename, self.getInfo()))
        buff.writeIndentedLines(code)

    def writeExpSetupCodeJS(self, buff, version):

        # write the code to set up experiment
        buff.setIndentLevel(0, relative=False)
        template = readTextFile("JS_setupExp.tmpl")
        setRedirectURL = ''
        if len(self.params['Completed URL'].val) or len(self.params['Incomplete URL'].val):
            setRedirectURL = ("psychoJS.setRedirectUrls({completedURL}, {incompleteURL});\n"
                              .format(completedURL=self.params['Completed URL'],
                                      incompleteURL=self.params['Incomplete URL']))
        # check where to save data variables
        # if self.params['OSF Project ID'].val:
        #     saveType = "OSF_VIA_EXPERIMENT_SERVER"
        #     projID = "'{}'".format(self.params['OSF Project ID'].val)
        # else:
        #     saveType = "EXPERIMENT_SERVER"
        #     projID = 'undefined'
        code = template.format(
                        params=self.params,
                        name=self.params['expName'].val,
                        loggingLevel=self.params['logging level'].val.upper(),
                        setRedirectURL=setRedirectURL,
                        version=version,
                        )
        buff.writeIndentedLines(code)

    def writeStartCode(self, buff, version):

        if not PY3:
            decodingInfo = ".decode(sys.getfilesystemencoding())"
        else:
            decodingInfo = ""
        code = ("# Ensure that relative paths start from the same directory "
                "as this script\n"
                "_thisDir = os.path.dirname(os.path.abspath(__file__))"
                "{decoding}\n"
                "os.chdir(_thisDir)\n\n"
                "# Store info about the experiment session\n"
                "psychopyVersion = '{version}'\n".format(decoding=decodingInfo,
                                                         version=version))
        buff.writeIndentedLines(code)

        if self.params['expName'].val in [None, '']:
            buff.writeIndented("expName = 'untitled.py'\n")
        else:
            code = ("expName = %s  # from the Builder filename that created"
                    " this script\n")
            buff.writeIndented(code % self.params['expName'])

        if PY3:  # in Py3 dicts are chrono-sorted
            sorting = "False"
        else:  # in Py2, with no natural order, at least be alphabetical
            sorting = "True"
        expInfoDict = self.getInfo()
        buff.writeIndented("expInfo = %s\n" % repr(expInfoDict))
        if self.params['Show info dlg'].val:
            buff.writeIndentedLines(
                f"dlg = gui.DlgFromDict(dictionary=expInfo, "
                f"sortKeys={sorting}, title=expName)\n"
                f"if dlg.OK == False:\n"
                f"    core.quit()  # user pressed cancel\n"
            )
        buff.writeIndentedLines(
            "expInfo['date'] = data.getDateStr()  # add a simple timestamp\n"
            "expInfo['expName'] = expName\n"
            "expInfo['psychopyVersion'] = psychopyVersion\n")
        level = self.params['logging level'].val.upper()

        saveToDir = self.getSaveDataDir()
        buff.writeIndentedLines("\n# Data file name stem = absolute path +"
                                " name; later add .psyexp, .csv, .log, etc\n")
        # deprecated code: before v1.80.00 we had 'Saved data folder' param
        # fairly fixed filename
        if 'Saved data folder' in self.params:
            participantField = ''
            for field in ('participant', 'Participant', 'Subject', 'Observer'):
                if field in expInfoDict:
                    participantField = field
                    self.params['Data filename'].val = (
                        repr(saveToDir) + " + os.sep + '%s_%s' % (expInfo['" +
                        field + "'], expInfo['date'])")
                    break
            if not participantField:
                # no participant-type field, so skip that part of filename
                self.params['Data filename'].val = repr(
                    saveToDir) + " + os.path.sep + expInfo['date']"
            # so that we don't overwrite users changes doing this again
            del self.params['Saved data folder']

        # now write that data file name to the script
        if not self.params['Data filename'].val:  # i.e., the user deleted it
            self.params['Data filename'].val = (
                repr(saveToDir) +
                " + os.sep + u'psychopy_data_' + data.getDateStr()")
        # detect if user wanted an absolute path -- else make absolute:
        filename = self.params['Data filename'].val.lstrip('"\'')
        # (filename.startswith('/') or filename[1] == ':'):
        if filename == os.path.abspath(filename):
            buff.writeIndented("filename = %s\n" %
                               self.params['Data filename'])
        else:
            buff.writeIndented("filename = _thisDir + os.sep + %s\n" %
                               self.params['Data filename'])

        # set up the ExperimentHandler
        code = ("\n# An ExperimentHandler isn't essential but helps with "
                "data saving\n"
                "thisExp = data.ExperimentHandler(name=expName, version='',\n"
                "    extraInfo=expInfo, runtimeInfo=None,\n"
                "    originPath=%s,\n")
        buff.writeIndentedLines(code % repr(self.exp.expPath))

        code = ("    savePickle=%(Save psydat file)s, saveWideText=%(Save "
                "wide csv file)s,\n    dataFileName=filename)\n")
        buff.writeIndentedLines(code % self.params)

        if self.params['Save log file'].val:
            code = ("# save a log file for detail verbose info\nlogFile = "
                    "logging.LogFile(filename+'.log', level=logging.%s)\n")
            buff.writeIndentedLines(code % level)
        buff.writeIndented("logging.console.setLevel(logging.WARNING)  "
                           "# this outputs to the screen, not a file\n")

        if self.exp.settings.params['Enable Escape'].val:
            buff.writeIndentedLines("\nendExpNow = False  # flag for 'escape'"
                                    " or other condition => quit the exp\n")

        buff.writeIndented("frameTolerance = 0.001  # how close to onset before 'same' frame\n")

    def writeIohubCode(self, buff):
        # Substitute inits
        inits = self.params.copy()
        if inits['mgMove'].val == "CONTINUOUS":
            inits['mgMove'].val = "$"

        # Make ioConfig dict
        code = (
            "\n"
            "# Setup eyetracking\n"
        )
        buff.writeIndentedLines(code % inits)
        if self.params['eyetracker'] == "None":
            code = (
                "ioDevice = ioConfig = ioSession = ioServer = eyetracker = None\n"
            )
            buff.writeIndentedLines(code % inits)
        else:
            # Alert user if window is not fullscreen
            if not self.params['Full-screen window'].val:
                alert(code=4540)
            # Alert user if no monitor config
            if self.params['Monitor'].val in ["", None, "None"]:
                alert(code=4545)
            # Alert user if they need calibration and don't have it
            if self.params['eyetracker'].val != "MouseGaze":
                if not any(isinstance(rt, EyetrackerCalibrationRoutine)
                           for rt in self.exp.flow):
                    alert(code=4510, strFields={"eyetracker": self.params['eyetracker'].val})
            # Write code
            code = (
                "ioDevice = '" + ioDeviceMap[self.params['eyetracker'].val] + "'\n"
                "ioConfig = {\n"
            )
            buff.writeIndentedLines(code % inits)
            buff.setIndentLevel(1, relative=True)
            code = (
                "ioDevice: {\n"
            )
            buff.writeIndentedLines(code % inits)
            buff.setIndentLevel(1, relative=True)
            code = (
                    "'name': 'tracker',\n"
            )
            buff.writeIndentedLines(code % inits)
            # Initialise for MouseGaze
            if self.params['eyetracker'] == "MouseGaze":
                code = (
                        "'controls': {\n"
                )
                buff.writeIndentedLines(code % inits)
                buff.setIndentLevel(1, relative=True)
                code = (
                            "'move': [%(mgMove)s],\n"
                            "'blink':%(mgBlink)s,\n"
                            "'saccade_threshold': %(mgSaccade)s,\n"
                )
                buff.writeIndentedLines(code % inits)
                buff.setIndentLevel(-1, relative=True)
                code = (
                        "}\n"
                )
                buff.writeIndentedLines(code % inits)
                buff.setIndentLevel(-1, relative=True)
                code = (
                    "}\n"
                )
                buff.writeIndentedLines(code % inits)

            elif self.params['eyetracker'] == "GazePoint":
                code = (
                        "'network_settings': {\n"
                )
                buff.writeIndentedLines(code % inits)
                buff.setIndentLevel(1, relative=True)
                code = (
                            "'ip_address': %(gpAddress)s,\n"
                            "'port': %(gpPort)s\n"
                )
                buff.writeIndentedLines(code % inits)
                buff.setIndentLevel(-1, relative=True)
                code = (
                        "}\n"
                )
                buff.writeIndentedLines(code % inits)
                buff.setIndentLevel(-1, relative=True)
                code = (
                    "}\n"
                )
                buff.writeIndentedLines(code % inits)

            elif self.params['eyetracker'] == "Tobii Technology":
                code = (
                        "'model_name': %(tbModel)s,\n"
                        "'serial_number': %(tbSerialNo)s,\n"
                        "'runtime_settings': {\n"
                )
                buff.writeIndentedLines(code % inits)
                buff.setIndentLevel(1, relative=True)
                code = (
                            "'sampling_rate': %(tbSampleRate)s,\n"
                )
                buff.writeIndentedLines(code % inits)
                buff.setIndentLevel(-1, relative=True)
                code = (
                        "}\n"
                )
                buff.writeIndentedLines(code % inits)
                buff.setIndentLevel(-1, relative=True)
                code = (
                    "}\n"
                )
                buff.writeIndentedLines(code % inits)

            elif self.params['eyetracker'] == "SR Research Ltd":
                code = (
                    "'model_name': %(elModel)s,\n"
                    "'simulation_mode': %(elSimMode)s,\n"
                    "'network_settings': %(elAddress)s,\n"
                    "'default_native_data_file_name': 'EXPFILE',\n"
                    "'runtime_settings': {\n"
                )
                buff.writeIndentedLines(code % inits)
                buff.setIndentLevel(1, relative=True)
                code = (
                        "'sampling_rate': %(elSampleRate)s,\n"
                        "'track_eyes': %(elTrackEyes)s,\n"
                        "'sample_filtering': {\n"
                )
                buff.writeIndentedLines(code % inits)
                buff.setIndentLevel(1, relative=True)
                code = (
                            "'sample_filtering': %(elDataFiltering)s,\n"
                            "'elLiveFiltering': %(elLiveFiltering)s,\n"
                )
                buff.writeIndentedLines(code % inits)
                buff.setIndentLevel(-1, relative=True)
                code = (
                        "},\n"
                        "'vog_settings': {\n"
                )
                buff.writeIndentedLines(code % inits)
                buff.setIndentLevel(1, relative=True)
                code = (
                            "'pupil_measure_types': %(elPupilMeasure)s,\n"
                            "'tracking_mode': %(elTrackingMode)s,\n"
                            "'pupil_center_algorithm': %(elPupilAlgorithm)s,\n"
                )
                buff.writeIndentedLines(code % inits)
                buff.setIndentLevel(-1, relative=True)
                code = (
                        "}\n"
                )
                buff.writeIndentedLines(code % inits)
                buff.setIndentLevel(-1, relative=True)
                code = (
                    "}\n"
                )
                buff.writeIndentedLines(code % inits)
                buff.setIndentLevel(-1, relative=True)
                code = (
                    "}\n"
                )
                buff.writeIndentedLines(code % inits)

            # Close ioConfig dict
            buff.setIndentLevel(-1, relative=True)
            code = (
                "}\n"
            )
            buff.writeIndentedLines(code % inits)

            # Start iohub server
            code = (
                "ioSession = '1'\n"
                "if 'session' in expInfo:\n"
            )
            buff.writeIndentedLines(code % inits)
            buff.setIndentLevel(1, relative=True)
            code = (
                    "ioSession = str(expInfo['session'])\n"
            )
            buff.writeIndentedLines(code % inits)
            buff.setIndentLevel(-1, relative=True)
            if self.params['Save hdf5 file'].val:
                saveStr = " experiment_code=%(expName)s, session_code=ioSession, datastore_name=filename,"
            else:
                saveStr = ""
            code = (
                f"ioServer = io.launchHubServer(window=win,{saveStr} **ioConfig)\n"
                f"eyetracker = ioServer.getDevice('tracker')\n"
            )
            buff.writeIndentedLines(code % inits)

        # Make default keyboard
        code = (
            "\n"
            "# create a default keyboard (e.g. to check for escape)\n"
            "defaultKeyboard = keyboard.Keyboard()\n"
        )
        buff.writeIndentedLines(code % self.params)

    def writeWindowCode(self, buff):
        """Setup the window code.
        """
        buff.writeIndentedLines("\n# Setup the Window\n")
        # get parameters for the Window
        fullScr = self.params['Full-screen window'].val
        # if fullscreen then hide the mouse, unless its requested explicitly
        allowGUI = (not bool(fullScr)) or bool(self.params['Show mouse'].val)
        allowStencil = False
        # NB routines is a dict:
        for thisRoutine in list(self.exp.routines.values()):
            # a single routine is a list of components:
            for thisComp in thisRoutine:
                if thisComp.type == 'Aperture':
                    allowStencil = True
                if thisComp.type == 'RatingScale':
                    allowGUI = True  # to have a mouse

        requestedScreenNumber = int(self.params['Screen'].val)
        nScreens = 10
        # try:
        #     nScreens = wx.Display.GetCount()
        # except Exception:
        #     # will fail if application hasn't been created (e.g. in test
        #     # environments)
        #     nScreens = 10
        if requestedScreenNumber > nScreens:
            logging.warn("Requested screen can't be found. Writing script "
                         "using first available screen.")
            screenNumber = 0
        else:
            # computer has 1 as first screen
            screenNumber = requestedScreenNumber - 1

        size = self.params['Window size (pixels)']
        winType = self.exp.prefsGeneral['winType']

        code = ("win = visual.Window(\n    size=%s, fullscr=%s, screen=%s, "
                "\n    winType='%s', allowGUI=%s, allowStencil=%s,\n")
        vals = (size, fullScr, screenNumber, winType, allowGUI, allowStencil)
        buff.writeIndented(code % vals)

        code = ("    monitor=%(Monitor)s, color=%(color)s, "
                "colorSpace=%(colorSpace)s,\n")
        if self.params['blendMode'].val:
            code += "    blendMode=%(blendMode)s, useFBO=True, \n"

        if self.params['Units'].val != 'use prefs':
            code += "    units=%(Units)s"
        code = code.rstrip(', \n') + ')\n'
        buff.writeIndentedLines(code % self.params)

        # Import here to avoid circular dependency!
        from psychopy.experiment._experiment import RequiredImport
        microphoneImport = RequiredImport(importName='microphone',
                                          importFrom='psychopy',
                                          importAs='')
        if microphoneImport in self.exp.requiredImports:  # need a pyo Server
            buff.writeIndentedLines("\n# Enable sound input/output:\n"
                                    "microphone.switchOn()\n")

        code = ("# store frame rate of monitor if we can measure it\n"
                "expInfo['frameRate'] = win.getActualFrameRate()\n"
                "if expInfo['frameRate'] != None:\n"
                "    frameDur = 1.0 / round(expInfo['frameRate'])\n"
                "else:\n"
                "    frameDur = 1.0 / 60.0  # could not measure, so guess\n")
        buff.writeIndentedLines(code)

    def writeWindowCodeJS(self, buff):
        """Setup the JS window code.
        """
        # Replace instances of 'use prefs'
        units = self.params['Units'].val
        if units == 'use prefs':
            units = 'height'

        code = ("// init psychoJS:\n"
                "const psychoJS = new PsychoJS({{\n"
                "  debug: true\n"
                "}});\n\n"
                "// open window:\n"
                "psychoJS.openWindow({{\n"
                "  fullscr: {fullScr},\n"
                "  color: new util.Color({params[color]}),\n"
                "  units: '{units}',\n"
                "  waitBlanking: true\n"
                "}});\n").format(fullScr=str(self.params['Full-screen window']).lower(),
                                 params=self.params,
                                 units=units)
        buff.writeIndentedLines(code)

    def writeEndCode(self, buff):
        """Write code for end of experiment (e.g. close log file).
        """
        code = ('\n# Flip one final time so any remaining win.callOnFlip() \n'
                '# and win.timeOnFlip() tasks get executed before quitting\n'
                'win.flip()\n\n')
        buff.writeIndentedLines(code)

        buff.writeIndented("# these shouldn't be strictly necessary "
                           "(should auto-save)\n")
        if self.params['Save wide csv file'].val:
            buff.writeIndented("thisExp.saveAsWideText(filename+'.csv', "
                               "delim={})\n".format(self.params['Data file delimiter']))
        if self.params['Save psydat file'].val:
            buff.writeIndented("thisExp.saveAsPickle(filename)\n")
        if self.params['Save log file'].val:
            buff.writeIndented("logging.flush()\n")
        code = ("# make sure everything is closed down\n"
                "thisExp.abort()  # or data files will save again on exit\n"
                "win.close()\n"
                "core.quit()\n")
        buff.writeIndentedLines(code)

    def writeEndCodeJS(self, buff):
        endLoopInteration = ("\nfunction endLoopIteration(scheduler, snapshot) {\n"
                    "  // ------Prepare for next entry------\n"
                    "  return async function () {\n"
                    "    if (typeof snapshot !== 'undefined') {\n"
                    "      // ------Check if user ended loop early------\n"
                    "      if (snapshot.finished) {\n"
                    "        // Check for and save orphaned data\n"
                    "        if (psychoJS.experiment.isEntryEmpty()) {\n"
                    "          psychoJS.experiment.nextEntry(snapshot);\n"
                    "        }\n"
                    "        scheduler.stop();\n"
                    "      } else {\n"
                    "        const thisTrial = snapshot.getCurrentTrial();\n"
                    "        if (typeof thisTrial === 'undefined' || !('isTrials' in thisTrial) || thisTrial.isTrials) {\n"
                    "          psychoJS.experiment.nextEntry(snapshot);\n"
                    "        }\n"
                    "      }\n"
                    "    return Scheduler.Event.NEXT;\n"
                    "    }\n"
                    "  };\n"
                    "}\n")
        buff.writeIndentedLines(endLoopInteration)

        recordLoopIterationFunc = ("\nfunction importConditions(currentLoop) {\n"
                    "  return async function () {\n"
                    "    psychoJS.importAttributes(currentLoop.getCurrentTrial());\n"
                    "    return Scheduler.Event.NEXT;\n"
                    "    };\n"
                    "}\n")
        buff.writeIndentedLines(recordLoopIterationFunc)

        code = ("\nasync function quitPsychoJS(message, isCompleted) {\n")
        buff.writeIndented(code)
        buff.setIndentLevel(1, relative=True)
        code = ("// Check for and save orphaned data\n"
                "if (psychoJS.experiment.isEntryEmpty()) {\n"
                "  psychoJS.experiment.nextEntry();\n"
                "}\n")
        buff.writeIndentedLines(code)

        # Write End Experiment code component
        for thisRoutine in list(self.exp.routines.values()):
            # a single routine is a list of components:
            for thisComp in thisRoutine:
                if thisComp.type == 'Code':
                    buff.writeIndented("\n")
                    thisComp.writeExperimentEndCodeJS(buff)
                    buff.writeIndented("\n")

        code = ("psychoJS.window.close();\n"
                "psychoJS.quit({message: message, isCompleted: isCompleted});\n\n"
                "return Scheduler.Event.QUIT;\n")
        buff.writeIndentedLines(code)

        buff.setIndentLevel(-1, relative=True)
        buff.writeIndented("}\n")
        buff.setIndentLevel(-1)

    @property
    def monitor(self):
        """Stores a monitor object for the  experiment so that it
        doesn't have to be fetched from disk repeatedly"""
        # remember to set _monitor to None periodically (start of script build?)
        # so that we do reload occasionally
        if not self._monitor:
            self._monitor = Monitor(self.params['Monitor'].val)
        return self._monitor

    @monitor.setter
    def monitor(self, monitor):
        self._monitor = monitor
