#!/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

#moa specific libs
import moa.job
import moa.utils
import moa.version
import moa.project
import moa.plugin
import moa.commands

#the system configuration is available as a global from the
#sysConf module
from moa.sysConf import sysConf

## 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
## Initialze the plugins
plugins = moa.plugin.PluginHandler(sysConf)

## Command definitions
sysConf.commands = moa.commands.Commands()
plugins.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="Verbose output")

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("-r", "--recursive", dest="recursive",
                  help="Perfom action recursively", action='store_true')

## See if the plugins have anything to add to the optparse instance:
sysConf.parser = parser
plugins.run('defineOptions')

def run_4(job, command):
    """
    Run the command
    """

    #see if it is all callback
    if command in sysConf.commands:
        if not sysConf.commands[command].has_key('call'):
            moa.ui.exitError("Invalid command - no callback %s" % command) 

        l.debug("using a callback for moa %s" % command)
        plugins.runCallback(job, command)
        return 0
    elif not job.isMoa():
        moa.ui.exitError("Unknown command, not in a Moa dir")
    else:
        #No callback, defer this to the backend
        return job.execute(command,
                           verbose = sysConf.options.verbose,
                           silent = sysConf.options.silent)
    
def run_3(wd):
    """
    instantiate the job & prepare &

    figure out if there is a set of commands to be executed. If so -
    fire those of one by one
    
    """
    os.chdir(wd)

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

    #create a job!
    job = moa.job.Job(wd)
    sysConf.job = job

    ## Aks the job & backend if they want to add to the options
    try:
        job.defineOptions(parser)
    except optparse.OptionConflictError:
        pass
    
    ## Parse the options
    sysConf.options, sysConf.args = parser.parse_args()

    sysConf.job = job

    ## 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:
        command = 'status'
        newargs = []

    sysConf.newargs = newargs
    sysConf.originalCommand = command        

    ## Ask the job if it's is ok with
    ## the command provided (might want to change order, or insert
    ## stuff
    execList = job.checkCommands(command)
    sysConf.executeCommand = execList

    l.debug("Run moa commands: %s" % ",".join(execList))
    l.debug("with args %s" % newargs)

    ### Start job initialization
    job.prepare()

    ### Run plugin initialization step 3 - just before execution
    plugins.run('prepare_3')

    plugins.run("preCommand")

    #run a prep step if the original command is not in the
    #execlist
    if command not in execList:
        plugins.run("pre%s" % command.capitalize())

    #run through all commands...
    for execNow in execList:

        l.debug("Executing %s" % execNow)                
        plugins.run("pre%s" % execNow.capitalize())

        sysConf.rc += abs(run_4(job, execNow))

        plugins.run("post%s" % execNow.capitalize(), reverse=True)

    #likewise if the command is not in the execlist - run a
    #post process tep
    if command not in execList:
        plugins.run("post%s" % command.capitalize(), reverse=True)

    plugins.run("postCommand", reverse=True)
    plugins.run("finish", reverse=True)

def run_recursive(wd):
    """
    Run through the subdirs (depth first) and execute all moa jobs
    """
    plugins.run('prepare_recursive')
    for path, dirs, files in os.walk(wd):
        if '.moa' in dirs:
            moa.ui.message("Executing 'moa' in %s" % (path))
            run_3(path)
        #remove all '.' directories -
        drem = [ x for x in dirs if x[0] == '.' ]
        [ dirs.remove(t) for t in drem]
    plugins.run('post_recursive')

    
def run_2():
    """
    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'
    """
    
    plugins.run('prepare_2')

    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', 'none')
        else:
            recurseMode = 'global'
            
        if recurseMode == 'global':
            run_recursive(wd)
        else:
            run_3(wd)
    plugins.run('post_2')


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

    if --bg is defined: fork & exit.

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

    #go to the next stage!
    run_2()

    plugins.run('post_1')

## Main dispatcher
if __name__ == "__main__":
    """
    Main run routing - not much has been prepared yet
    """
    try:
        run_1()
    except KeyboardInterrupt:
        sysConf.rc = -1
        plugins.run("post_interrupt")
        moa.ui.warn("Interrupted")
        sys.exit(-1)
    except:
        sysConf.rc = -1
        plugins.run("post_error")
        moa.ui.warn("Error")
        raise

