#! /usr/bin/python3.2
import sys
import os
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.DEBUG)
##

import subprocess
import shutil
import glob
import re
import tempfile

VERSION = "0.5.2"

#Python version must be egal or bigger than...
MIN_PYTHON_VERSION = "3.2"
#Python version must be smaller than...
STRICT_MAX_PYTHON_VERSION = "4.0"

#Blender version must be egal or bigger than...
MIN_BLENDER_VERSION = "2.59"
#Blender version must be smaller than...
STRICT_MAX_BLENDER_VERSION = "2.63"

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

#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):
    try:
        version_str = subprocess.Popen(
                                [blender_path, '--version'], 
                                stdout=subprocess.PIPE).communicate()[0]
        version_str = version_str.decode()
        version = version_str.split()[1] + '.' + version_str.split()[3][:-1]
    except OSError:
        return None
    
    logger.info("Checking version of " + blender_path + "... Found v." + version)
    
    if  version.split('.') >= MIN_BLENDER_VERSION.split('.') and \
        version.split('.') < STRICT_MAX_BLENDER_VERSION.split('.') :
        return version
    else:
        return False

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
    """
    with tempfile.NamedTemporaryFile() as tmpF:
        tmpF.write(b"import sys\n")
        tmpF.write(b"print('>>>' + '.'.join((str(x) for x in sys.version_info[:2])))\n")
        tmpF.flush()
    
        try:
            version_str = subprocess.Popen(
                      [blender_path, '-b', '-P', tmpF.name], 
                      stdout=subprocess.PIPE).communicate()[0]
            version_str = version_str.decode()
            version = version_str.split('>>>')[1][0:3]
        except (OSError, IndexError):
            return None
    
        logger.info("Checking version of Python within Blender " + blender_path + \
                        "... Found v." + version)
    
        return version
        
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 check_setup():
    """
    Checks that the environment is correctly setup to run MORSE.
    Raises exceptions when an error is detected.
    """
    
    global morse_prefix, blender_exec, default_scene_abspath
    
    ###########################################################################
    #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
        
    
    
    ###########################################################################
    #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(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 + \
                " and < " + STRICT_MAX_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 not (python_version.split('.') >= MIN_PYTHON_VERSION.split('.') and 
            python_version.split('.') < STRICT_MAX_PYTHON_VERSION.split('.')):
        logger.error("Blender is using Python " + python_version + \
        ". Python  >= " + MIN_PYTHON_VERSION + " and < " + STRICT_MAX_PYTHON_VERSION + \
        " is required to run MORSE. Note that Blender usually provides its own " + \
        "Python runtime that may differ from the system one.")
        raise MorseError("Bad Python version")
    else:
        logger.info("Blender is using Python " + python_version + \
        ". Alright.")

def create_copy_default_scene(filename = None):
    """
    Creates a copy of the default scene in the current path, ensuring an 
    unique name.
    """
    
    global default_scene_abspath
    
    if not filename:
        previous_scenes = glob.glob("scene.*.blend")
        num_list = [0]
        for scene in previous_scenes:
            try:
                num = re.findall('[0-9]+', scene)[0]
                num_list.append(int(num))
            except IndexError:
                pass
        num_list = sorted(num_list)
        new_num = num_list[-1]+1
        new_scene = os.path.join(os.curdir, 'scene.%02d.blend' % new_num)
    else:
        new_scene = os.path.normpath(filename)

    shutil.copy(default_scene_abspath, new_scene)
    
    return new_scene

def prelaunch():
    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 informations.\n" +\
        "You can also run 'morse check' for more details.")
        sys.exit()

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


    :param list script_options: if specified, elements of this list are passed
            to the Python script (and are accessible via sys.argv)
    """

    global morse_prefix, blender_exec, default_scene_abspath
    
    logger.info("*** Launching MORSE ***\n")
    logger.info("PREFIX= " + morse_prefix)
    
    if not scene:
        if not script: # in case of no argument, execute a default script
            script = default_scene_abspath[:-5]+'py'
        scene = create_copy_default_scene()
        logger.info("Creating new scene " + scene)
        
    elif not os.path.exists(scene):
        logger.error(scene + " does not exist!\nIf you want to create a new scene " + \
        "called " + scene + ",\nplease use 'morse create " + scene + "'.")
        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 the 
    # default system paths (like '/usr/lib/python*')
    env = os.environ
    env["PYTHONPATH"] = ":".join(sys.path)

    # Add the MORSE node name to env.
    if node_name != None:
        env["MORSE_NODE"] = node_name
    
    #Replace the current process by Blender
    if script != None:
        logger.info("Executing Blender script: " + script)
        if script_options:
            script_options.append(env)
            os.execle(blender_exec, blender_exec, scene, "-P", script, "--", *script_options)
        else:
            os.execle(blender_exec, blender_exec, scene, "-P", script, env)
    else:
        os.execle(blender_exec, blender_exec, scene, env)

def version():
    print("morse " + VERSION + "\nCopyright LAAS-CNRS 2011\n")

def help(cmd=None):
    
    if not cmd:
        print ("""morse [command] [options]

Known commands:
  [None]                 launchs the simulator interface with a default scene.
  create filename        creates a new empty scene and launchs the simulator
                         interface.
  run filename [options] runs a simulation without loading the simulator 
                         interface.
  exec filename          runs the given Python script with a default scene.
  check                  checks the environment is correctly setup to run morse.
  help                   displays this message and exits.
  version                displays the version number and exits.

'morse' followed by a Blender file name opens the simulator with this scene.

Use help [command] to get more help on a specific command.""")
        return

    if cmd == "run":
        print ("""morse run file_name [script options]

Runs a simulation without loading the simulator interface. Equivalent
to the sequence [open a Blend file in the simulator, switch to 
fullscreen, press P].

file_name can be either a Blender file containing a simulation already
set up or a Python script using the MORSE Builder API.

In the later case, optional options can be passed to the script.
""")
    else:
        print("No help for \"" + cmd + "\".")
        help()

if __name__ == '__main__':

    
    if len(sys.argv) > 1:
        
        if sys.argv[1] in ["help", "--help", "-h"]:
            if len(sys.argv) > 2:
                help(sys.argv[2])
            else:
                help()
            sys.exit()
        elif sys.argv[1] in ["version", "--version", "-v"]:
            version()
            sys.exit()
        elif sys.argv[1] in ["check"]:
            try:
                logger.info("Checking up your environment...\n")
                check_setup()
            except MorseError as e:
                logger.error("Your environment is not correctly setup to run MORSE!")
                sys.exit()
            logger.info("Your environment is correctly setup to run MORSE.")
            
        elif sys.argv[1] in ["create"]:
            if len(sys.argv) == 3:
                prelaunch()
                launch_simulator(create_copy_default_scene(sys.argv[2]))                
            else:
                logger.error("'create' option expect exactly one option (a filename)")
                sys.exit()
        elif sys.argv[1] in ["run"]:
            if len(sys.argv) >= 3:
                prelaunch()
                script_options = sys.argv[3:]
                launch_simulator(
                   os.path.join(morse_prefix, DEFAULT_SCENE_AUTORUN_PATH), \
                   sys.argv[2], \
                   script_options = script_options)
            else:
                logger.error("'run' option expect exactly one option (a filename)")
                sys.exit()
        elif sys.argv[1] in ["exec"]:
            if len(sys.argv) < 3:
                logger.error("'exec' option expect at least one option (a filename)")
                sys.exit()
            prelaunch()
            node_name = os.uname()[1]
            if len(sys.argv) > 3:
                node_name = sys.argv[3]
            launch_simulator(script=sys.argv[2], node_name=node_name)
            
        else:
            scene = None
            script = None
            args = sys.argv[1:]
            for arg in args:
                if arg.endswith(".blend"):
                    scene = arg
                    break
            for arg in args:
                if arg.endswith(".py"):
                    script = arg
                    break
            if not scene and not script:
                logger.error("Unknown option: " + str(args))
                help()
                sys.exit()
            prelaunch()
            launch_simulator(scene, script)
    
    else:
        
        prelaunch()
        launch_simulator()
