#!/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 sos_run, sos_inspect, sos_prepare, sos_convert, sos_config

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+B`).'''
    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 inspect 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.'''
    #
    # 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=1, 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('-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 inspect mode to check syntax error, prepare mode
            to prepare resources, and run mode to execute the pipelines. Run mode
            options allow you to check the script or download resources without
            actually running the workflow.''')
    runmode.add_argument('-d', action='store_true', dest='__debug_mode__',
        help=argparse.SUPPRESS)
    runmode.add_argument('-i', action='store_true', dest='__inspect__',
        help='''Execute the workflow in inspect mode in which step processes
            are executed normally but with most SoS actions return directly.
            SoS also produces diagnoistic warning messages in this mode.''')
    runmode.add_argument('-p', action='store_true', dest='__prepare__',
        help='''Execute the workflow in dyrun mode, and then 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=sos_run)
    #
    # command inspect
    parser = subparsers.add_parser('inspect',
        description='''Execute a workflow in inspect mode. In comparison to
            run mode, SoS does not panic for non-existing input or output
            files, does not execute most SoS actions, and does not save or
            check runtime signatures. It simply goes through the workflow,
            figure out input/output of steps, and produce warning messages
            if necessary.''',
        aliases=['dryrun'],
        epilog=workflow_options,
        help='Execute a SoS script in inspect mode')
    parser.add_argument('script', metavar='SCRIPT', help=script_help)
    parser.add_argument('workflow', metavar='WORKFLOW', nargs='?',
        help=workflow_spec)
    parser.add_argument('-t', dest='__transcript__', nargs='?',
        metavar='TRANSCRIPT', const='__STDERR__', help=transcript_help)
    #parser.add_argument('options', metavar='WORKFLOW_OPTIONS',
    #    nargs=argparse.REMAINDER, help=workflow_options)
    addCommonArgs(parser)
    parser.set_defaults(func=sos_inspect)
    #
    # 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('-t', dest='__transcript__', nargs='?',
        metavar='TRANSCRIPT', const='__STDERR__', help=transcript_help)
    #parser.add_argument('options', metavar='WORKFLOW_OPTIONS',
    #    nargs=argparse.REMAINDER, help=workflow_options)
    addCommonArgs(parser)
    parser.set_defaults(func=sos_prepare)
    #
    # command show
    #
    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. ''',
        aliases=['show', 'view'],
        help='View a SoS script or workflow in various formats')
    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=sos_convert)
    #
    # command config
    #
    parser = subparsers.add_parser('config',
        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=sos_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)
