#! /usr/bin/python3
import sys
import os
import platform
import logging
## Configuring default logging
try:
    from morse.core.ansistrm import ColorizingStreamHandler
    log_handler = ColorizingStreamHandler()
except ImportError:
    log_handler = logging.StreamHandler()

logger = logging.getLogger('morse')

formatter = logging.Formatter("* %(message)s\n")
log_handler.setFormatter(formatter)

logger.addHandler(log_handler)
logger.setLevel(logging.INFO)
##

try:
    sys.path.append("/usr/lib/python3/dist-packages")
    from morse.version import VERSION
    from morse.environments import Environment
    from morse.core.exceptions import MorseEnvironmentError
except ImportError as detail:
    logger.error("Unable to continue: '%s'\nVerify that your PYTHONPATH variable points to the MORSE installed libraries" % detail)
    sys.exit()

import subprocess
import shutil
import glob
import re
import tempfile

try:
    import configparser
    import argparse
except ImportError as exn:
    logger.error("Cannot import %s" % exn)
    sys.exit()

#Blender version must be egal or bigger than...
MIN_BLENDER_VERSION = "2.65"
#Blender version must be smaller than...
LAST_TESTED_BLENDER_VERSION = "2.77"

#Unix-style path to the MORSE default scene and templates, within the prefix
DEFAULT_SCENE_PATH = "share/morse/data/morse_default.blend"
DEFAULT_SCENE_AUTORUN_PATH = "share/morse/data/morse_default_autorun.blend"

DEFAULT_SCENE_NAME = "default.py"

#MORSE prefix (automatically detected)
morse_prefix = ""
#Path to Blender executable (automatically detected)
blender_exec = ""
#Path to MORSE default scene (automatically detected)
default_scene_abspath = ""

class MorseError(Exception):
    def __init__(self, value):
        self.value = value
    def __str__(self):
        return repr(self.value)

def retrieve_blender_from_path():
    try:
        blenders_in_path = subprocess.Popen(
                                ['which', '-a', 'blender'],
                                stdout=subprocess.PIPE).communicate()[0]
        res = blenders_in_path.decode().splitlines()
    except OSError:
        return []

    return res

def check_blender_version(blender_path):
    version = None
    try:
        version_str = subprocess.Popen(
                                [blender_path, '--version'],
                                stdout=subprocess.PIPE).communicate()[0]
        for line in version_str.splitlines():
            line = line.decode().strip()
            if line.startswith("Blender"):
                version = line.split()[1] + '.' + line.split()[3][:-1]
                break
    except OSError:
        return None

    if not version:
        logger.error("Could not recognize Blender version!" \
                     "Please copy the output of " +  blender_path + \
                     " --version and send it to morse-dev@laas.fr.")
        return None

    logger.info("Checking version of " + blender_path + "... Found v." + version)

    if version.split('.') < MIN_BLENDER_VERSION.split('.'):
        return False
    if version.split('.') > LAST_TESTED_BLENDER_VERSION.split('.'):
        logger.warning("Version %s of Blender is untested but should work" % version)
    return version

def check_blender_python_version(blender_path):
    """ Creates a small Python script to execute within Blender and get the
    current Python version bundled with Blender
    """
    tmpF = tempfile.NamedTemporaryFile(delete = False)
    try:
        tmpF.write(b"import sys\n")
        tmpF.write(b"print('>>>' + '.'.join((str(x) for x in sys.version_info[:3])))\n")
        tmpF.write(b"print('###%s' % sys.path)\n")
        tmpF.flush()
        tmpF.close()
        output = subprocess.Popen(
            [blender_path, '-b', '-P', tmpF.name],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE
        ).communicate() # get only stdout
        stdout = output[0].decode()
        stderr = output[1].decode()
        version = stdout.split('>>>')[1][0:5]
        logger.info("Checking version of Python within Blender " + blender_path + \
                        "... Found v." + version)

        if stderr:
            logger.warning("Blender emitted errors during launch:\n" + stderr)

        return version
    except (OSError, IndexError):
            return None
    finally:
        tmpF.close()
        os.unlink (tmpF.name)

def check_default_scene(prefix):

    global default_scene_abspath
    #Check morse_default.blend is found
    default_scene_abspath = os.path.join(os.path.normpath(prefix), os.path.normpath(DEFAULT_SCENE_PATH))

    #logger.info("Looking for the MORSE default scene here: " + default_scene_abspath)

    if not os.path.exists(default_scene_abspath):
        raise MorseError(default_scene_abspath)
    else:
        return default_scene_abspath

def set_prefixes(silent = False):
    """
    Checks that the environment is correctly setup to run MORSE.
    Raises exceptions when an error is detected.
    """

    global morse_prefix

    loglevel = logger.getEffectiveLevel()
    if silent:
        logger.setLevel(logging.WARNING)

    ###########################################################################
    #Check platform
    if not 'linux' in sys.platform:
        logger.warning("MORSE has only been tested on Linux. It may work\n" + \
        "on other operating systems as well, but without any guarantee")
    else:
        logger.info("Running on Linux. Alright.")

    ###########################################################################
    #Check PYTHONPATH variable

    found = False
    for dir in sys.path:
        if os.path.exists(os.path.join(dir, "morse/blender/main.py")):
            logger.info("Found MORSE libraries in '" + dir + "/morse/blender'. Alright.")
            found = True
            break

    if not found:
        logger.error(  "We could not find the MORSE Python libraries in your\n" +\
                        "system. If MORSE was installed to some strange location,\n" + \
                        "you may want to add it to your PYTHONPATH.\n" + \
                        "Check INSTALL for more details.")
        raise MorseError("PYTHONPATH not set up.")
    ###########################################################################
    #Detect MORSE prefix
    #-> Check for $MORSE_ROOT, then script current prefix
    try:
        prefix = os.environ['MORSE_ROOT']
        logger.info("$MORSE_ROOT environment variable is set. Checking for default scene...")

        check_default_scene(prefix)
        logger.info("Default scene found. The prefix seems ok. Using it.")
        morse_prefix = prefix

    except MorseError:
        logger.warning("Couldn't find the default scene from $MORSE_ROOT prefix!\n" + \
        "Did you move your installation? You should fix that!\n" + \
        "Trying to look for alternative places...")
    except KeyError:
        pass

    if morse_prefix == "":
        #Trying to use the script location as prefix (removing the trailing '/bin'
        # if present)
        logger.info("Trying to figure out a prefix from the script location...")
        prefix = os.path.abspath(os.path.dirname(sys.argv[0]))
        if prefix.endswith('bin'):
            prefix = prefix[:-3]

        try:
            check_default_scene(prefix)

            logger.info("Default scene found. The prefix seems ok. Using it.")
            morse_prefix = prefix
            os.environ['MORSE_ROOT'] = prefix
            logger.info("Setting $MORSE_ROOT environment variable to default prefix [" + prefix + "]")

        except MorseError as me:
            logger.error("Could not find the MORSE default scene (I was expecting it\n" + \
                    "there: " + me.value + ").\n" + \
                    "If you've installed MORSE files in an exotic location, check that \n" + \
                    "the $MORSE_ROOT environment variable points to MORSE root directory.\n" + \
                    "Else, try to reinstall MORSE.")
            raise

        if silent:
            #restoring initial logger level
            logger.setLevel(loglevel)

def check_setup():

    global blender_exec
    global morse_prefix

    set_prefixes()

    ###########################################################################
    #Check Blender version
    #First, look for the $MORSE_BLENDER env variable
    try:
        blender_exec = os.environ['MORSE_BLENDER']
        version = check_blender_version(blender_exec)
        if version:
            logger.info("Blender found from $MORSE_BLENDER. Using it (Blender v." + \
            version + ")")
        elif version == False:
            blender_exec = ""
            logger.warning("The $MORSE_BLENDER environment variable points to an " + \
            "incorrect version of\nBlender! You should fix that! Trying to look " + \
            "for Blender in alternative places...")
        elif version == None:
            blender_exec = ""
            logger.warning("The $MORSE_BLENDER environment variable doesn't point " + \
            "to a Blender executable! You should fix that! Trying to look " + \
            "for Blender in alternative places...")
    except KeyError:
        pass

    if blender_exec == "":
        #Then, check the version of the Blender executable in the path
        for blender_path in retrieve_blender_from_path():
            blender_version_path = check_blender_version(blender_path)

            if blender_version_path:
                blender_exec = blender_path
                logger.info("Found Blender in your PATH\n(" + blender_path + \
                ", v." + blender_version_path + ").\nAlright, using it.")
                break

        #Eventually, look for another Blender in the MORSE prefix
        if blender_exec == "":
            blender_prefix = os.path.join(os.path.normpath(morse_prefix), os.path.normpath("bin/blender"))
            blender_version_prefix = check_blender_version(blender_prefix)

            if blender_version_prefix:
                blender_exec = blender_prefix
                logger.info("Found Blender in your prefix/bin\n(" + blender_prefix + \
                ", v." + blender_version_prefix + ").\nAlright, using it.")

            else:
                logger.error("Could not find a correct Blender executable, neither in the " + \
                "path or in MORSE\nprefix. Blender >= " + MIN_BLENDER_VERSION + \
                " is required to run MORSE.\n" + \
                "You can alternatively set the $MORSE_BLENDER environment variable " + \
                "to point to\na specific Blender executable")
                raise MorseError("Could not find Blender executable")

    ###########################################################################
    #Check Python version within Blender
    python_version = check_blender_python_version(blender_exec)
    if python_version == None:
        logger.warn("Blender's Python version could not be determined. "
                    "Crossing fingers.")
    else:
        our_python_version = '.'.join((str(x) for x in sys.version_info[:3]))
        if (python_version != our_python_version):
            logger.error("Blender is compiled for Python " + python_version + \
                   " but MORSE has been compiled for Python " + \
                   our_python_version + "! Check your MORSE build configuration"
                   " or the selected Blender version.")
            if 'MORSE_SILENT_PYTHON_CHECK' not in os.environ:
                raise MorseError("Bad Python version")
        else:
            logger.info("Blender and Morse are using Python " + \
            python_version + ". Alright.")

def get_config_file():
    config_path = os.path.expanduser("~/.morse")
    if not os.path.exists(config_path):
        os.mkdir(config_path)

    return os.path.join(config_path, "config")

def add_site(name, path):
    config = configparser.SafeConfigParser()
    config.read(get_config_file())

    if not config.has_section("sites"):
        config.add_section("sites")

    config.set('sites', name, path)

    with open(get_config_file(), 'w') as configfile:
        config.write(configfile)

def get_site(name):
    """ Returns the absolute path to a simulation environment specified
    in ~/.morse/config, if it exists, None otherwise.
    """
    config = configparser.SafeConfigParser()
    config.read(get_config_file())

    if not config.has_option("sites", name):
        return None

    return config.get('sites', name)

def delete_site(name):
    config = configparser.SafeConfigParser()
    config.read(get_config_file())

    if config.has_option("sites", name):
        config.remove_option("sites", name)

    with open(get_config_file(), 'w') as configfile:
        config.write(configfile)


def set_morse_environ(site):

        logger.info('Adding <%s> to PYTHONPATH and MORSE_RESOURCE_PATH.' % site)

        sys.path.insert(0, os.path.join(site, 'src'))

        if 'MORSE_RESOURCE_PATH' in os.environ:
                # pre-pend the site path to be sure we shadow existing resources
            os.environ['MORSE_RESOURCE_PATH'] = "%s" % os.path.join(site, 'data') + os.pathsep + os.environ['MORSE_RESOURCE_PATH']
        else:
            os.environ['MORSE_RESOURCE_PATH'] = "%s" % os.path.join(site, 'data')



def get_scene(name, subname = ""):
    """ Tries to figure out which Builder script the user is refering to
    and return the absolute path to it.

    If only one parameter *name* is given:

    - if *name* is a configured simulation environment with prefix $ENVROOT:
        - if '$ENVROOT/default.py' exists, launch it.
        - else, if any file with an extension {.py|.blend} exists, launch the first
            one (in alphanumerical order)
        - else if a file called *name* exists in the current directory, launch it.
        - else, fail
    - else check if a file called *name* exists, and launch it (note that in that
    case, *name* can contain an absolute path or a path relative to the current
    directory).

    If two parameters *name* and *subname* are given:

    - if *name* is a configured simulation environment with prefix $ENVROOT:
        - if *$ENVROOT/subname* exists, launch it
        - else, add $ENVROOT to MORSE environment, and if *subname* exists, launch it (note
          that *subname* can contain an absolute path or a path relative to the current
          directory)
    - else fail
    """

    if not subname:

        site = get_site(name)
        if site:
            default = os.path.join(site, DEFAULT_SCENE_NAME)
            set_morse_environ(site)

            if os.path.exists(default):
                logger.info('Using default scene in environment <%s>.' % name)
                return default

            logger.info('Using environment <%s>.' % name)

            candidates = [os.path.join(site, f) for f in os.listdir(site) if re.match(r'.*\.(py|blend)', f)]
            candidates = sorted([c for c in candidates if os.path.isfile(c)]) # filters out directories
            if candidates:
                logger.warning('No scene specified. Launching MORSE with first found '
                               'in environment: <%s>.' % candidates[0])
                logger.info('You can specify another scene by passing its name as'
                            ' parameter after the simulation environment.')
                return sortedcandidates[0]

        if os.path.exists(name) and os.path.isfile(name):
            logger.info('Loading simulation <%s>.' % os.path.abspath(name))
            return os.path.abspath(name)

        logger.error('Cannot find a MORSE environment or simulation '
                     'scene matching <%s>!' % name)

    else: #both name and subname are defined

        site = get_site(name)
        if site:
            logger.info('Using environment <%s>.' % name)
            set_morse_environ(site)

            candidate = os.path.join(site, subname)
            if os.path.exists(candidate) and os.path.isfile(candidate):
                logger.info('Loading simulation <%s>.' % candidate)
                return candidate
            elif os.path.exists(subname) and os.path.isfile(subname):
                logger.info('Loading simulation <%s>.' % os.path.abspath(subname))
                return os.path.abspath(subname)


            logger.error('Simulation <%s> not found! (I was looking for <%s> or <%s>)' % (subname, candidate, os.path.abspath(subname)))
        else:
            logger.error('MORSE environment <%s> does not exist!' % name)

def create_environment(args):

    name = args.env
    force = args.force

    set_prefixes(silent = True)

    site = get_site(name)
    if not force and site:
        logger.error("You already have a simulation environment "
                     "called \"%s\" (pointing to <%s>)!" % (name, site))
        logger.error("Use 'morse create -f <env>' to overwrite it.")
        sys.exit()


    add_site(name, os.path.abspath(name))

    env = Environment(morse_prefix, name)

    env.create(force)

    logger.info("A new simulation environment has been successfully "
                "created in <%s>." % os.path.abspath(name))
    logger.info("You can run it directly with \"morse run %s\" or you "
                "can start editing it." % name)

def import_environment(args):


    path = os.path.abspath(args.path)
    name = args.name
    if not name:
        name = os.path.basename(path)

    force = args.force

    site = get_site(name)
    if not force and site:
        logger.error("You already have a simulation environment "
                     "called \"%s\" (pointing to <%s>)!" % (name, site))
        logger.error("Use 'morse import -f <path>' to overwrite it.")
        sys.exit()


    add_site(name, path)

    logger.info("The simulation environment <%s> has been successfully "
                "imported." % name)
    logger.info("You can run it directly with \"morse run %s\" or you "
                "can start editing it." % name)


def delete_environment(args):

    name = args.env
    force = args.force

    site = get_site(name)
    if not site:
        logger.error("Found no simulation environment called \"%s\"" % name)
        sys.exit()

    if force:
        ok = 'y'
    else:
        ok = input("Are you sure you want to delete the environment \"%s\" [y/N]" % name)

    if ok.lower() == 'y':
        if os.path.exists(site):
            shutil.rmtree(site)
        delete_site(name)
        logger.info("Environment \"%s\" successfully deleted." % name)

def add_component(args):

    type = args.type
    name = args.name
    env = args.env
    force = args.force

    set_prefixes(silent = True)


    site = get_site(env)
    if not site:
        logger.error("No simulation environment called \"%s\"!" % env)
        logger.error("Use 'morse create %s' to create it first." % env)
        sys.exit()

    env = Environment(morse_prefix, env, site)

    try:
        env.add_component(type, name, force = force)
    except MorseEnvironmentError as e:
        logger.error("%s" % e)
        sys.exit(1)

    logger.debug("A new %s called <%s> has been added to <%s>." % (type, name, args.env))


def prelaunch():
    logger.info(version())
    try:
        logger.setLevel(logging.WARNING)
        logger.info("Checking up your environment...\n")
        check_setup()
    except MorseError as e:
        logger.error("Your environment is not yet correctly setup to run MORSE!\n" +\
        "Please fix it with above information.\n" +\
        "You can also run 'morse check' for more details.")
        sys.exit()

def launch_simulator(scene=None, script=None, node_name=None, geometry=None,
        noaudio = False, script_options = []):
    """Starts Blender on an empty new scene or with a given scene.

    :param tuple geometry: if specified, a tuple (width, height, dx, dy) that
            specify the Blender window geometry. dx and dy are the distance in
            pixels from the lower left corner. They can be None (in this case, they
            default to 0).
    :param list script_options: if specified, elements of this list are passed
            to the Python script (and are accessible with MORSE scripts via
            sys.argv)
    """

    logger.info("*** Launching MORSE ***\n")
    logger.info("PREFIX= " + morse_prefix)

    if not os.path.exists(scene):
        logger.error(scene + " does not exist!\nIf you want to create a new scene " + \
        "called " + scene + ",\nplease create a Python script using the Builder API, run 'morse edit [script_filename]' and save as a .blend file.")
        sys.exit(1)

    logger.info("Executing: " + blender_exec + " " + scene + "\n\n")

    #Flush all outputs before launching Blender process
    sys.stdout.flush()

    # Redefine the PYTHONPATH to include both the user-set path plus
    # default system paths (like '/usr/lib/python*')
    env = os.environ
    env["PYTHONPATH"] = os.pathsep.join(sys.path)

    # Add the MORSE node name to env.
    if node_name:
        env["MORSE_NODE"] = node_name
        logger.info("Setting MORSE_NODE to " + env["MORSE_NODE"])

    # Prepare the geometry
    if geometry:
        w,h,dx,dy = geometry
        # -w for a window with borders, -p for the window geometry
        other_params = ["-w", "-p", dx if dx else '0', dy if dy else '0', w, h]
    else:
        other_params = ["-w"]

    if script_options:
        other_params.append('--')
        other_params += script_options

    other_params.append(env) # must be the last option
    blender_exec_encoded = os.fsencode(blender_exec)
    exec_args = [blender_exec_encoded, blender_exec]

    if noaudio:
        exec_args += ["-setaudio", "NULL"]

    exec_args += [scene, "-y"]
    exec_args += ["--no-native-pixels"]

    #Replace the current process by Blender
    if script is not None:
        if os.name == 'nt':
            # need a temporary file because PYTHONPATH is disrespected
            tmpF = tempfile.NamedTemporaryFile(delete = False)
            try:
                script_path = os.path.abspath(script)
                script_path = script_path.replace ('\\', '/')
                tmpF.write(("import sys\n").encode())
                for element in sys.path:
                    element = element.replace('\\', '/')
                    tmpF.write(("sys.path.append('" + element + "')\n").encode())
                tmpF.write(("exec(open('" + script_path + "').read())\n").encode())
                tmpF.write(("import os\n").encode())
                tmpF.flush()
                tmpF.close()
                script = tmpF.name
            finally:
                # if the temporary file is still open then an error
                # occured and we need to delete it here, else we should
                # not touch it
                if not tmpF.closed:
                    tmpF.close()
        logger.info("Executing Blender script: " + script)
        exec_args += ["-P", script]

    exec_args += other_params
    os.execle(*exec_args)

def do_check(args):
    try:
        logger.info("Checking up your environment...\n")
        check_setup()
    except MorseError as e:
        logger.error(e.value)
        logger.error("Your environment is not correctly setup to run MORSE!")
        sys.exit()
    logger.info("Your environment is correctly setup to run MORSE.")


def process_run_edit(args):

    script_options = args.pyoptions
    if args.color:
        script_options.append("with-colors")
    if args.reverse_color:
        script_options.append("with-colors")
        script_options.append("with-reverse-colors")

    # xmas mode!
    import datetime
    today = datetime.date.today()
    if today > datetime.date(today.year, 12, 25):
        script_options.append("with-xmas-colors")

    if args.mode == "run":
        if not args.env:
            logger.error("'run' mode requires a simulation environment and/or a simulation scene (.py or .blend)")
            sys.exit()

        scene = get_scene(args.env, args.file) or sys.exit(1)

        prelaunch()
        if scene.endswith(".blend"):
            launch_simulator(
                   scene, \
                   geometry=args.geom, \
                   node_name=args.name, \
                   noaudio=args.noaudio, \
                   script_options = script_options)
        else:
            launch_simulator(
                   os.path.join(morse_prefix, DEFAULT_SCENE_AUTORUN_PATH), \
                   scene, \
                   geometry=args.geom, \
                   node_name=args.name, \
                   noaudio=args.noaudio, \
                   script_options = script_options)

    else:  # args.mode == "edit":
        if not args.env:
            logger.error("'edit' mode requires a simulation environment and/or a simulation scene to edit (.blend or .py)")
            sys.exit()

        scene = get_scene(args.env, args.file) or sys.exit(1)

        prelaunch()

        if scene.endswith(".blend"):
            launch_simulator(
                   scene, \
                   geometry=args.geom, \
                   node_name=args.name, \
                   noaudio=args.noaudio, \
                   script_options = script_options)
        else:
            base_scene = args.base if 'base' in vars(args) else default_scene_abspath
            launch_simulator(
                   scene = base_scene, \
                   script = scene, \
                   geometry=args.geom, \
                   node_name=args.name, \
                   noaudio=args.noaudio, \
                   script_options = script_options)

def version():
    return("morse " + VERSION)

if __name__ == '__main__':

    def parsegeom(string):
        dx = dy = None
        try:
             w, remain = string.split('x')
             if '+' in remain:
                h, remain = remain.split('+')
                dx, dy = remain.split(',')
             else:
                h = remain
        except ValueError:
             raise argparse.ArgumentTypeError("The geometry must be formatted as WxH or WxH+dx,dy")
        return (w,h,dx,dy)

    # Check whether MORSE_NODE is defined
    default_morse_node = platform.uname()[1]
    try:
        default_morse_node = os.environ["MORSE_NODE"]
    except:
        pass

    parser = argparse.ArgumentParser(description='The Modular OpenRobots Simulation Engine')

    # First, general MORSE options
    parser.add_argument('-n', '--noaudio', action='store_true',
                       help='dont look for sound card')
    parser.add_argument('-c', '--color', action='store_true',
                       help='uses colors for MORSE output.')
    parser.add_argument('--reverse-color', action='store_true',
                       help='uses darker colors for MORSE output.')
    parser.add_argument('-v', '--version', action='version',
                       version=version(), help='returns the current MORSE version')



    ## Then, the sub-commands

    subparsers = parser.add_subparsers(title="modes", description="type 'morse <mode> --help' for details", dest = "mode")

    # create
    create_parser = subparsers.add_parser('create', help="creates a new simulation environment in the current directory.")
    create_parser.add_argument('env', default="", help="the name of the environment to create.")
    create_parser.add_argument('-f', '--force', action='store_true',
                       help='forces the creation (possibly overwriting files).')
    create_parser.set_defaults(func=create_environment)

    # import
    import_parser = subparsers.add_parser('import', help="imports a pre-existing simulation environment.")
    import_parser.add_argument('path', default="", help="the relative or absolute path to the environment. For instance './lab_sim_checkout'.")
    import_parser.add_argument('name', default="", nargs='?', help="the name of the environment (extracted from path if not specified).")
    import_parser.add_argument('-f', '--force', action='store_true',
                       help='forces the import (possibly overwriting existing environment).')
    import_parser.set_defaults(func=import_environment)


    # rm
    rm_parser = subparsers.add_parser('rm', help="deletes an existing simulation environment.")
    rm_parser.add_argument('env', default="", help="the environment to delete.")
    rm_parser.add_argument('-f', '--force', action='store_true',
                       help='silently delete.')
    rm_parser.set_defaults(func=delete_environment)


    # add
    add_parser = subparsers.add_parser('add', help="adds a template for a component to an environment.")
    add_parser.add_argument('type', choices=['robot', 'sensor', 'actuator'],
            help="the type of component you want to add.")
    add_parser.add_argument('name', default="", help="the name of the new component.")
    add_parser.add_argument('env', default="", help="the environment where to add.")
    add_parser.add_argument('-f', '--force', action='store_true',
                       help='overwrite existing component with the same name.')
    add_parser.set_defaults(func=add_component)



    # check
    check_parser = subparsers.add_parser('check', help="checks your environment is correctly configured.")
    check_parser.set_defaults(func=do_check)

    # run
    run_parser = subparsers.add_parser('run', help="starts a simulation.")
    run_parser.add_argument('env', default="", nargs='?',
            help="the simulation environment to run.")
    run_parser.add_argument('file', default="", nargs='?',
            help="the exact scene (.py or .blend) to run (if 'env' is given, within this environment). Refer to manual for details.")
    run_parser.add_argument('pyoptions', nargs=argparse.REMAINDER,
                       help="optional parameters, passed to the Blender python engine in sys.argv")
    run_parser.add_argument('--name', default=default_morse_node,
                       help="when running in multi-node mode, sets the name of this node (defaults "
                            "to either MORSE_NODE if defined or current hostname).")
    run_parser.add_argument('-g', '--geometry', dest='geom', type=parsegeom, action='store',
                       help="sets the simulator window geometry. Expected format: WxH or WxH+dx,dy "
                            "to set an initial x,y delta (from lower left corner).")
    run_parser.set_defaults(func=process_run_edit)

    # edit
    edit_parser = subparsers.add_parser('edit', help="opens an existing scene for edition.")
    edit_parser.add_argument('env', default="", nargs='?',
            help="the simulation environment to edit.")
    edit_parser.add_argument('file', default="", nargs='?',
            help="the exact scene (.py or .blend) to edit (if 'env' is given, within this environment). Refer to manual for details.")
    edit_parser.add_argument('pyoptions', nargs=argparse.REMAINDER,
                       help="optional parameters, passed to the Blender python engine in sys.argv")
    edit_parser.add_argument('-b', '--base', dest='BASE',
                       help='specify a Blender scene used as base to apply the Python script.')
    edit_parser.add_argument('--name', default=default_morse_node,
                       help="when running in multi-node mode, sets the name of this node (defaults "
                            "to either MORSE_NODE if defined or current hostname).")
    edit_parser.add_argument('-g', '--geometry', dest='geom', type=parsegeom, action='store',
                       help="sets the simulator window geometry. Expected format: WxH or WxH+dx,dy "
                            "to set an initial x,y delta (from lower left corner).")
    edit_parser.set_defaults(func=process_run_edit)



    args = parser.parse_args()
    try:
        args.func(args)
    except AttributeError:
        parser.print_usage()
