#!/usr/bin/env python
# 
# Copyright 2009, 2010 Mark Fiers, Plant & Food Research
# 
# This file is part of Moa - http://github.com/mfiers/Moa
# 
# Moa is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your
# option) any later version.
# 
# Moa is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
# License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with Moa.  If not, see <http://www.gnu.org/licenses/>.
# 
"""
Moa executable

This script is a command dispatcher for all Moa functionality.

Run order

 * define & load plugins
 * plugin run 'defineCommands'
 * plugin run 'defineOptions'
 * __main__
 
    * plugin run 'prepare_1'
    * bg - then fork
       * Forking
       
    
 *    
 * prepare_1_hook

 1) check if we're going to backgound - if yes, do so & exit
 2) check if this is a recursive run - if yes - start recursion

"""

import os
import sys
import optparse
import textwrap

# check we have the proper version of python

if sys.version_info < (2, 6):
    raise "Must use python 2.6 or greater"

# the system configuration is available as a global for the
# rest of the system

from moa.sysConf import sysConf
sysConf.initialize()

# moa specific libs
import moa.job
import moa.utils
import moa.actor
import moa.version
import moa.plugin
import moa.commands
import moa.exceptions

## Initialize the logger
import moa.logger as l

## A hack to set verbosity before reading command line arguments
if '-v' in sys.argv: moa.logger.setVerbose()

sysConf.rc = 0

## Command definitions
sysConf.commands = moa.commands.Commands()
sysConf.pluginHandler.run('defineCommands')

## defining a usage message
USAGE = "Usage: {{bold}}{{green}}%prog {{blue}}[options] COMMAND ARGUMENTS{{reset}}\n\n"
moaCommandNames = sysConf.commands.keys()
moaCommandNames.sort()

commandsG = []
commandsJ = []

for _c in moaCommandNames:
    if sysConf.commands[_c].get('private', False):
        continue
    if sysConf.commands[_c].get('needsJob',  True):
        commandsJ.append([_c, sysConf.commands[_c]['desc']])
    else:
        commandsG.append([_c, sysConf.commands[_c]['desc']])

commandsG.sort()
commandsJ.sort()
USAGE += "Universal Moa commands:\n"
for c, d in commandsG:
    USAGE += "\n".join(
        textwrap.wrap(
            '{{bold}}%15s{{reset}} : %s' % (c, d),
            initial_indent=' ',
            subsequent_indent = ' ' * 19)) + "\n"

USAGE += "\nJob specific Moa commands:\n"
for c,d  in commandsJ:
    USAGE += "\n".join(
        textwrap.wrap(
            '{{bold}}%15s{{reset}} : %s' % (c, d),
            initial_indent=' ',
            subsequent_indent = ' ' * 19)) + "\n"


## define & parse command line options  
parser = optparse.OptionParser(usage=moa.ui.fformat(USAGE, f='jinja'))

parser.add_option(
    '-f', '--force', dest='force', action='store_true',
    help='Force an action, if applicable.')

parser.add_option(
    "-v", "--verbose", dest="verbose", action="store_true",
    help="Show debugging output")

parser.add_option(
    '-a', '--all', action='store_true', dest='showAll', 
    help = 'Show more - depends on context')

parser.add_option(
    "-s", "--silent", dest="silent", action="store_true",
    help="Be very silent")

parser.add_option(
    "--bg", dest="background", action="store_true",
    help="Run moa in the background (implies -s)")

parser.add_option(
    "--profile", dest="profile", action="store_true",
    help="Run the profiler")

parser.add_option("-r", "--recursive", dest="recursive",
                  help="Perfom action recursively", action='store_true')

## See if the pluginHandler have anything to add to the optparse instance:
sysConf.parser = parser
sysConf.pluginHandler.run('defineOptions')
    
def run_3(wd, exitOnError=True):
    """
    instantiate the job, prepare and run.

    Running means that either a plugin callback is called or the main
    job is executed. Main job execution consists of three steps:
    
    - prepare
    - run
    - finish

    prepare, run, finish should probably not be overridden by plugin
    callbacks. It is possible to run prepare & finish separately.

    """

    #finally we have a directory to run in - see if this job want to
    #be executed..

    #make sure we're in the correct wd for the rest of the invocation
    os.chdir(wd)

    #see if we can update the job
    moa.version.fixOld(wd)

    #create the job
    job = moa.job.Job(wd)

    #check if we should run this job..
    #TODO - 
    #doRun = sysConf.pluginHandler.run('check_run')

    sysConf.pluginHandler.run('prepare_3')
    
    ## Aks the job & backend if they want to add to the options
    try:
        job.defineOptions(parser)
    except optparse.OptionConflictError:
        pass
    
    ## Only now the full set of options is parsed 
    sysConf.options, sysConf.args = parser.parse_args()

    ## Proper setting of verbosity - after parsing of the command line
    if sysConf.options.verbose:
        moa.logger.setVerbose()
    
    ## Determine command to run:
    if len(sysConf.args) > 0:
        command = sysConf.args[0]
        newargs = sysConf.args[1:]
    else:
        #default command
        command = sysConf.default_command
        newargs = []

    sysConf.newargs = newargs
    sysConf.originalCommand = command        

    #see if this is all callback
    if command in sysConf.commands:

        l.debug("running plugin callback for %s" % command)
        sysConf.pluginHandler.execute(command)

    else:
        if not job.isMoa():
            if exitOnError:
                moa.ui.exitError("Must run 'moa %s' in a Moa dir" % command)
            else:
                return False
            
        if command == 'run':
            job.execute(
                verbose = sysConf.options.verbose,
                silent = sysConf.options.silent)
        elif command == 'prepare':
            job.prepare()
        elif command == 'finish':
            job.finish()
        elif job.hasCommand(command):
            job.simpleExecute(command)
        else:            
            if exitOnError:
                moa.ui.exitError("Unknown command '%s'" % command)
            else:
                moa.ui.warn("Unknown command '%s'" % command)
                return False


def run_recursive(wd):
    """
    Run through the subdirs (depth first) and execute all moa jobs
    """
    sysConf.pluginHandler.run('prepare_recursive')
    for path, dirs, files in os.walk(wd):
        dirs.sort() #make sure the dirs are sorted
        if '.moa' in dirs:
            if os.path.exists(os.path.join(path, '.moa', 'template')):
                clargs = " ".join(sys.argv)
                moa.ui.message("Executing '%s' .." % clargs)
                moa.ui.message("  .. in %s" % (path))
                rc = run_3(path, exitOnError=False)
                if rc == False:
                    moa.ui.message("Error running %s" % clargs)
            
        #remove all '.' directories -
        drem = [ x for x in dirs if x[0] in ['.', '_'] ]
        [ dirs.remove(t) for t in drem]
    sysConf.pluginHandler.run('post_recursive')

    
def run_2(force_silent=False):
    """
    Are we going to do a recursive run?: check if -r is in the arguments...

    If recursive - check if the command given is a plugin callback or not
    plugin callbacks can have a recursive mode:

    * global - allow recursivity to be handled here
    * local - the callback handles recursive behaviour
    * none - no recursive operation for this template

    non plugin callbacks (commands handled by the backends) are always
    'global'

    """
    
    sysConf.pluginHandler.run('prepare_recursion')

    wd = os.getcwd()
    if not '-r' in sys.argv:
        #not recursive - go directly to stage 3
        run_3(wd)
    else:
        #default mode is global
        recurseMode = 'global'

        # check if the command allows for recursive execution
        # therefore - a quick & dirty approach to getting the
        # command name - we haven't properly parsed the
        # arguments yet
        command = sysConf.default_command
        tas = [x for x in sys.argv[1:] if not x[0] == '-']
        if len(tas) > 0: command = tas[0]

        if command in sysConf.commands:
            recurseMode = sysConf.commands[command].get('recursive', 'global')
        else:
            recurseMode = 'global'
            
        if recurseMode == 'global':
            run_recursive(wd)
        elif recurseMode == 'none':
            moa.ui.exitError("Recursive execution is not allowed")
        else:
            run_3(wd)

    sysConf.pluginHandler.run('post_recursion')


def run_1():
    """
    Stage 1 - are we switching to the background?

    if --bg is defined: fork & exit.

    """
    sysConf.pluginHandler.run('prepare_1')
    sysConf.pluginHandler.run('prepare_background')
    sysConf.force_silent=False
    #quick check: are we're backgrounding:
    if '--bg' in sys.argv:
        sysConf.force_silent=True
        child = os.fork()
        if child != 0:
            # This is the parent thread - exit now - all
            sysConf.childPid = child
            sysConf.pluginHandler.run("background_exit")
            moa.ui.message("starting background run")
            sys.exit(0)

    #go to the next stage!
    run_2()

    sysConf.pluginHandler.run('post_background')

def _handle_error( message):
    """
    Try to handle an error situation - i.e. run post_error
    """
    sys.stderr.write("ERROR!\n")
    sys.stderr.write(message + "\n")
    try:        
        sysConf.rc = -1
        sysConf.pluginHandler.run("post_error")
    except Exception, e:
        sys.stderr.write("Error - cannot run post_error plugin hook:\n")
        sys.stderr.write(str(e) + "\n\n")
    
## Main dispatcher
if __name__ == "__main__":
    """
    Main run routing - not much has been prepared yet
    """
    try:
        if '--profile' in sys.argv:
            import tempfile
            tf = tempfile.NamedTemporaryFile(delete=False)
            tf.close()
            try:
                import cProfile
                import pstats
                cProfile.run('run_1()', tf.name)
                p = pstats.Stats(tf.name)
                moa.ui.message("Profiler output")
                p.sort_stats('cumulative').print_stats(20)
                
            except ImportError:
                moa.ui.exitError("Cannot run the profiler - make sure it is properly installed")
            
        else:
            run_1()
    except KeyboardInterrupt:
        sysConf.rc = -1
        sysConf.pluginHandler.run("post_interrupt")
        moa.ui.warn("Interrupted")
        if sysConf.options.verbose:
            raise
        else:
            sys.exit(-2)
    except moa.exceptions.MoaDirNotWritable:
        _handle_error("the .moa directory is not writable") 
    except IOError, e:
        _handle_error("IOError - maybe the .moa directory is not writable")
        raise e
    except Exception, e:
        _handle_error("Unexpected error") 
        moa.ui.warn("Encountered a run error?")
        raise e

