#!/usr/bin/env python3
#
# This file is part of Script of Scripts (sos), a workflow system
# for the execution of commands and scripts in different languages.
# Please visit https://github.com/bpeng2000/SOS for more information.
#
# Copyright (C) 2016 Bo Peng (bpeng@mdanderson.org)
#
# This program 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.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
#

import sys
import argparse

from pysos._version import SOS_FULL_VERSION
from pysos.main import cmd_run, cmd_dryrun, cmd_prepare, cmd_convert, cmd_remove, cmd_config, cmd_start

def addCommonArgs(parser):
    parser.add_argument('-v', '--verbosity', type=int, choices=range(5), default=2,
            help='''Output error (0), warning (1), info (2), debug (3) and trace (4)
            information to standard output (default to 2).'''),

if __name__ == '__main__':
    master_parser = argparse.ArgumentParser(description='''A workflow system
            for the execution of commands and scripts in different languages.''',
        prog='sos',
        fromfile_prefix_chars='@',
        epilog='''Use 'sos cmd -h' for details about each subcommand. Please
            contact Bo Peng (bpeng at mdanderson.org) if you have any question.''')

    master_parser.add_argument('--version', action='version',
        version='%(prog)s {}'.format(SOS_FULL_VERSION))
    subparsers = master_parser.add_subparsers(title='subcommands')
    script_help = '''A SoS script that defines one or more workflows. The
        script can be a filename or a URL from which the content of a SoS will
        be read. If a valid file cannot be located or downloaded, SoS will
        search for the script in a search path specified by variable `sos_path`
        defined in the global SoS configuration file (~/.sos/config.yaml).'''
    workflow_spec =  '''Name of the workflow to execute. This option can be
        ignored if the script defines a default workflow (with no name or with
        name `default`) or defines only a single workflow. A subworkflow or a
        combined workflow can also be specified, where a subworkflow executes a
        subset of workflow (`name_steps` where `steps` can be `n` (a step `n`),
        `:n` (up to step `n`), `n:m` (from step `n` to `m`), and `n:` (from step
        `n`)), and a combined workflow executes to multiple (sub)workflows
        combined by `+` (e.g. `A_0+B+C`).'''
    workflow_options = '''Arbitrary parameters defined by the [parameters] step
        of the script, and [parameters] steps of other scripts if nested workflows
        are defined in other SoS files (option `source`). The name, default and
        type of the parameters are specified in the script. Single value parameters
        should be passed using option `--name value` and multi-value parameters
        should be passed using option `--name value1 value2`. '''
    transcript_help = '''Name of a file that records the execution transcript of
        the script. The transcript will be recorded in dryrun and run mode but
        might differ in content because of dynamic input and output of scripts.
        If the option is specified wiht no value, the transcript will be written
        to standard error output.'''
    bindir_help = '''Extra directories in which SoS will look for executables before
        standard $PATH. This option essentially prefix $PATH with these directories.
        Note that the default value '~/.sos/bin' is by convention a default
        directory for commands that are installed by SoS. You can use option '-b'
        without value to disallow commands under ~/.sos/bin.'''
    #
    # command run
    parser = subparsers.add_parser('run',
        description='Execute a workflow defined in script',
        epilog=workflow_options,
        help='Execute a SoS script')
    parser.add_argument('script', metavar='SCRIPT', help=script_help)
    parser.add_argument('workflow', metavar='WORKFLOW', nargs='?',
        help=workflow_spec)
    parser.add_argument('-j', type=int, metavar='JOBS', default=4, dest='__max_jobs__',
        help='''Number of concurrent process allowed. A workflow is by default
            executed sequentially (-j 1). If a greater than 1 number is specified
            SoS will execute the workflow in parallel mode and execute up to
            specified processes concurrently. These include looped processes
            within a step (with runtime option `concurrent=True`) and steps with
            non-missing required files.''')
    parser.add_argument('-c', dest='__config__', metavar='CONFIG_FILE',
        help='''A configuration file in the format of YAML/JSON. The content
            of the configuration file will be available as a dictionary
            CONF in the SoS script being executed.''')
    parser.add_argument('-t', dest='__targets__', metavar='FILE', default=[],
        nargs='+', help='''One of more files or alias of other targets that
            will be the target of execution. If specified, SoS will execute
            only part of a workflow or multiple workflows or auxiliary steps
            to generate specified targets.''')
    parser.add_argument('-b', dest='__bin_dirs__', nargs='*', metavar='BIN_DIR',
        default=['~/.sos/bin'], help=bindir_help)
    parser.add_argument('-q', dest='__queue__', metavar='QUEUE',
        help='''Task-processing queue. SoS by default uses a local multiprocessing
            queue where tasks are executed by different processes. Supported task
            queues include a 'rq' engine where tasks will be distributed to one or
            more rq-workers with assistance from a redis server, and a 'celery'
            quque where tasks will be distributed to celery workers.''')
    #parser.add_argument('-r', dest='__report__', metavar='REPORT_FILE',
    #    const='__STDOUT__', nargs='?',
    #    help='''Name of a file that records output from report lines
    #        (lines starts with !) and report action of the script. Report
    #        will be written to standard output if the option is specified
    #        without any value.''')
    #parser.add_argument('-t', dest='__transcript__', nargs='?',
    #    metavar='TRANSCRIPT', const='__STDERR__', help=transcript_help)
    runmode = parser.add_argument_group(title='Run mode options',
        description='''SoS scripts are by default executed in run mode where all
            the script is run in dryrun mode to check syntax error, prepare mode
            to prepare resources, and run mode to execute the pipelines. Run mode
            options allow you to execute these steps selectively.''')
    runmode.add_argument('-n', action='store_true', dest='__dryrun__',
        help='''Execute a workflow without executing any actions. This can be
            used to check the syntax of a SoS file.''')
    runmode.add_argument('-p', action='store_true', dest='__prepare__',
        help='''Execute the workflow in preparation mode in which SoS prepare
            the execution of workflow by, for example, download required
            resources and docker images.''')
    runmode.add_argument('-f', action='store_true', dest='__rerun__',
        help='''Execute the workflow in a special run mode that ignores saved
            runtime signatures and re-execute all the steps.''')
    runmode.add_argument('-F', action='store_true', dest='__construct__',
        help='''Execute the workflow in a special run mode that re-use existing
            output files and recontruct runtime signatures if output files
            exist.''')
    addCommonArgs(parser)
    parser.set_defaults(func=cmd_run)
    #
    # command dryrun
    parser = subparsers.add_parser('dryrun',
        description='''Inspect specified script for syntax errors''',
        epilog=workflow_options,
        help='Execute a SoS script in dryrun mode')
    parser.add_argument('script', metavar='SCRIPT', help=script_help)
    parser.add_argument('workflow', metavar='WORKFLOW', nargs='?',
        help=workflow_spec)
    parser.add_argument('-c', dest='__config__', metavar='CONFIG_FILE',
        help='''A configuration file in the format of YAML/JSON. The content
            of the configuration file will be available as a dictionary
            CONF in the SoS script being executed.''')
    parser.add_argument('-t', dest='__targets__', metavar='FILES', default=[],
        nargs='+', help='''One of more files or alias of other targets that
            will be the target of execution. If specified, SoS will execute
            only part of a workflow or multiple workflows or auxiliary steps
            to generate specified targets. ''')
    addCommonArgs(parser)
    parser.set_defaults(func=cmd_dryrun)
    #
    # command prepare
    parser = subparsers.add_parser('prepare',
        description='''Execute a workflow in prepare mode in which SoS
            prepares the exeuction of workflow by, for example, download
            required resources and docker images.''',
        epilog=workflow_options,
        help='Execute a SoS script in prepare mode')
    parser.add_argument('script', metavar='SCRIPT', help=script_help)
    parser.add_argument('workflow', metavar='WORKFLOW', nargs='?',
        help=workflow_spec)
    parser.add_argument('-c', dest='__config__', metavar='CONFIG_FILE',
        help='''A configuration file in the format of YAML/JSON. The content
            of the configuration file will be available as a dictionary
            CONF in the SoS script being executed.''')
    parser.add_argument('-t', dest='__targets__', metavar='FILES', default=[],
        nargs='+', help='''One of more files or alias of other targets that
            will be the target of execution. If specified, SoS will execute
            only part of a workflow or multiple workflows or auxiliary steps
            to generate specified targets. ''')
    parser.add_argument('-b', dest='__bin_dirs__', nargs='*', metavar='BIN_DIRS',
        default=['~/.sos/bin'], help=bindir_help)
    addCommonArgs(parser)
    parser.set_defaults(func=cmd_prepare)
    #
    # command convert
    #
    parser = subparsers.add_parser('convert',
        description='''The show command displays details of all workflows
            defined in a script, including description of script, workflow,
            steps, and command line parameters. The output can be limited
            to a specified workflow (which can be a subworkflow or a combined
            workflow) if a workflow is specified.''',
        epilog='''Extra command line argument could be specified to customize
            the style of html, markdown, and terminal output. ''',
        help='Convert between sos and other file formats such as html and Jupyter notebooks')
    parser.add_argument('from_file', metavar='FILENAME',
        help='''File to be converted, can be a SoS script or a Jupyter
            notebook.''')
    parser.add_argument('workflow', metavar='WORKFLOW', nargs='?',
        help='''Workflow to be converted if the file being converted is a SoS
            script.''')
    parser.add_argument('--html', nargs='?', metavar='FILENAME', const='__BROWSER__',
        help='''Generate a syntax-highlighted HTML file, write it to a
            specified file, or view in a browser if no filename is specified.
            Additional argument --raw can be used to specify a URL to raw file,
            arguments --linenos and --style can be used to customize style of
            html output. You can pass an arbitrary name to option --style get a
            list of available styles.''')
    parser.add_argument('--markdown', nargs='?', metavar='FILENAME', const='__STDOUT__',
        help='''Convert script or workflow to markdown format and write it to
            specified file, or standard output if not filename is specified.''')
    parser.add_argument('--term', action='store_true',
        help='''Output syntax-highlighted script or workflow to the terminal.
            Additional arguments --bg=light|dark --lineno can be used to
            customized output.''')
    parser.add_argument('--notebook', nargs='?', metavar='FILENAME', const='__STDOUT__',
        help='''Convert script or workflow to jupyter notebook format and write
            it to specified file, or standard output if no filename is specified.
            If the input file is a notebook, it will be converted to .sos (see
            option --sos) then to notebook, resetting indexes and removing all
            output cells.''')
    parser.add_argument('--sos', nargs='?', metavar='SCRIPT', const='__STDOUT__',
        help='''Convert specified Jupyter notebook to SoS format. The output
            is the same as you use File -> Download as -> SoS (.sos) from
            Jupyter with nbconvert version 4.2.0 or higher although you can
            customize output using options --reorder (rearrange notebook cells
            with execution order), --reset-index (reset indexes to 1, 2, 3, ..),
            --add-header (add section header [index] if the cell does not start
            with a header), --no-index (does not save cell index), --remove-magic
            (remove cell magic), and --md-to-report (convert markdown cell to
            code cell with report.)''')
    addCommonArgs(parser)
    parser.set_defaults(func=cmd_convert)
    #
    # command remove 
    #
    parser = subparsers.add_parser('remove',
        help='''Remove tracked and/or untracked files with their signatures''',
        description='''Remove specified files and directories and their
            signatures (if available). Optionally, you can remove only 
            tracked files (input, output and intermediate files of executed
            workflows) or untracked file from specified files and/or
            directories.''')
    parser.add_argument('targets', nargs='*', metavar='FILE_OR_DIR',
        help='''Files and directories to be removed, which should be under the
            current directory (default). All, tracked, or untracked files
            will be removed depending on other options ('-t' or '-u').
            For safety reasons, files under the current directory have to be
            listed (not as files under .) to be removed.''')
    group = parser.add_mutually_exclusive_group(required=False)
    group.add_argument('-t', action='store_true', dest='__tracked__', default=False,
        help='''Remove tracked files and their signatures from specified files
            and directories.''')
    group.add_argument('-u', action='store_true', dest='__untracked__', default=False,
        help='''Remove untracked files from specified files and directories.''')
    parser.add_argument('-n', action='store_true', dest='__dryrun__',
        help='''List files or directories to be removed, without actually
            removing them.''')
    parser.add_argument('-y', action='store_true', dest='__confirm__',
        help='''Remove files without confirmation, suitable for batch removal
            of files.''')
    parser.set_defaults(func=cmd_remove)
    #
    # command start
    #
    parser = subparsers.add_parser('start',
        description='''Start server or worker''')
    parser.add_argument('server_type', choices=('server', 'worker'), metavar='TYPE')
    parser.set_defaults(func=cmd_start)
    #
    # command config
    #
    parser = subparsers.add_parser('config',
        help='''Set, unset or get the value of system or local configuration files''',
        description='''The config command displays, set, and unset configuration
            variables defined in global or local configuration files.''')
    parser.add_argument('-g', '--global', action='store_true', dest='__global_config__',
        help='''If set, change global (~/.sos/config.yaml) instead of local
        (.sos/config.yaml) configuration''')
    parser.add_argument('-c', '--config', dest='__config_file__', metavar='CONFIG_FILE',
        help='''User specified configuration file in YAML format. This file will not be
        automatically loaded by SoS but can be specified using option `-c`''')
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('--get', nargs='*', metavar='OPTION', dest='__get_config__',
        help='''Display values of specified configuration. The arguments of this
        option can be a single configuration option or a list of option. Wildcard
        characters are allowed to match more options (e.g. '*timeout', quotation
        is needed to avoid shell expansion). If no option is given, all options
        will be outputted.''')
    group.add_argument('--unset', nargs='+', metavar='OPTION',  dest='__unset_config__',
        help='''Unset (remove) settings for specified options. The arguments of this
        option can be a single configuration option or a list of option. Wildcard
        characters are allowed to match more options (e.g. '*timeout', or '*' for
        all options, quotation is needed to avoid shell expansion).''')
    group.add_argument('--set', nargs='+', metavar='KEY VALUE', dest='__set_config__',
        help='''--set KEY VALUE sets VALUE to variable KEY. The value can be any valid
        python expression (e.g. 5 for integer 5 and '{"c": 2, "d": 1}' for a dictionary)
        with invalid expression (e.g. val without quote) considered as string. Syntax
        'A.B=v' can be used to add {'B': v} to dictionary 'A', and --set KEY VALUE1 VALUE2 ...
        will create a list with multiple values.''')
    addCommonArgs(parser)
    parser.set_defaults(func=cmd_config)
    #
    if len(sys.argv) == 1:
        master_parser.print_help()
        sys.exit(0)
    args, workflow_args = master_parser.parse_known_args()
    # calling the associated functions
    args.func(args, workflow_args)
