#!/usr/bin/env python3
# -*- coding: UTF-8 -*-

"""
ABOUT: This is the main entry for the pipeline.
REQUIRES:
  - python>=3.6
  - snakemake   (recommended>=6.0.0)
  - singularity (recommended==latest)
DISCLAIMER:
                    PUBLIC DOMAIN NOTICE
        NIAID Collaborative Bioinformatics Resource (NCBR)
   National Institute of Allergy and Infectious Diseases (NIAID)
This software/database is a "United  States Government Work" under
the terms of the United  States Copyright Act.  It was written as 
part of the author's official duties as a United States Government
employee and thus cannot be copyrighted. This software is freely
available to the public for use.
Although all  reasonable  efforts have been taken  to ensure  the
accuracy and reliability of the software and data, NCBR do not and
cannot warrant the performance or results that may  be obtained by 
using this software or data. NCBR and NIH disclaim all warranties,
express  or  implied,  including   warranties   of   performance, 
merchantability or fitness for any particular purpose.
Please cite the author and NIH resources like the "Biowulf Cluster" 
in any work or product based on this material.
USAGE:
  $ metavirs <run> [OPTIONS]
EXAMPLE:
  $ metavirs run --input *.R?.fastq.gz --output output/
"""

# Python standard library
from __future__ import print_function
from hashlib import md5
from shutil import unpack_archive
import sys, os, subprocess, re, json, textwrap

# 3rd party imports from pypi
import argparse  # potential python3 3rd party package, added in python/3.5

# Local imports
from src import version 
from src.run import init, setup, bind, dryrun, runner
from src.download import main as installer
from src.shells import bash
from src.utils import (
    Colors,
    err,
    exists,
    fatal,
    permissions,
    check_cache,
    require,
    cat
)


# Pipeline Metadata
__version__ = version
__authors__ = 'Skyler Kuhn, Paul Schaughency'
__email__ = 'skyler.kuhn@nih.gov, paul.schaughency@nih.gov'
__home__  =  os.path.dirname(os.path.abspath(__file__))
_name = os.path.basename(sys.argv[0])
_description = 'Viral metagenomic assembly and annotation pipeline'


def unlock(sub_args):
    """Unlocks a previous runs output directory. If snakemake fails ungracefully,
    it maybe required to unlock the working directory before proceeding again.
    This is rare but it does occasionally happen. Maybe worth add a --force
    option to delete the '.snakemake/' directory in the future.
    @param sub_args <parser.parse_args() object>:
        Parsed arguments for unlock sub-command
    """
    print("Unlocking the pipeline's output directory...")
    outdir = sub_args.output

    try:
        unlock_output = subprocess.check_output([
            'snakemake', '--unlock',
            '--cores', '1',
            '--configfile=config.json'
        ], cwd = outdir,
        stderr=subprocess.STDOUT)
    except subprocess.CalledProcessError as e:
        # Unlocking process returned a non-zero exit code
        sys.exit("{}\n{}".format(e, e.output))

    print("Successfully unlocked the pipeline's working directory!")


def run(sub_args):
    """Initialize, setup, and run the pipeline.
    Calls initialize() to create output directory and copy over pipeline resources,
    setup() to create the pipeline config file, dryrun() to ensure their are no issues
    before running the pipeline, and finally run() to execute the Snakemake workflow.
    @param sub_args <parser.parse_args() object>:
        Parsed arguments for run sub-command
    """
    # Step 0. Check for required dependencies
    # The pipelines has only two requirements:
    # snakemake and singularity 
    require(['snakemake', 'singularity'], ['snakemake', 'singularity'])

    # Step 1. Initialize working directory,
    # copy over required resources to run 
    # the pipeline
    git_repo = __home__
    input_files = init(
        repo_path = git_repo,
        output_path = sub_args.output,
        links = sub_args.input
    )

    # Step 2. Setup pipeline for execution, 
    # dynamically create config.json config
    # file from user inputs and base config
    # templates. Also override path of any 
    # reference files from default in the 
    # OpenOmics shared group area to base 
    # path provided by user via the option
    # --resource-bundle PATH
    config = setup(sub_args, 
        ifiles = input_files,
        repo_path = git_repo,
        output_path = sub_args.output,
        resource_bundle = sub_args.resource_bundle
    )
    
    # Step 3. Resolve docker/singularity bind
    # paths from the config file.
    bindpaths = bind(
        sub_args,
        config = config
    )

    # Optional Step: Dry-run pipeline
    if sub_args.dry_run:
        # Dryrun pipeline
        dryrun_output = dryrun(outdir = sub_args.output) # python3 returns byte-string representation
        print("\nDry-running {} pipeline:\n{}".format(_name, dryrun_output.decode("utf-8")))
        sys.exit(0)

    # Step 4. Orchestrate pipeline execution,
    # run pipeline in locally on a compute node
    # for debugging purposes or submit the master
    # job to the job scheduler, SLURM, and create 
    # logging file
    if not exists(os.path.join(sub_args.output, 'logfiles')):
        # Create directory for logfiles
        os.makedirs(os.path.join(sub_args.output, 'logfiles'))
    if sub_args.mode == 'local':
        log = os.path.join(sub_args.output, 'logfiles', 'snakemake.log')
    else: 
        log = os.path.join(sub_args.output, 'logfiles', 'master.log')
    logfh = open(log, 'w')
    mjob = runner(mode = sub_args.mode, 
        outdir = sub_args.output, 
        # additional_bind_paths = all_bind_paths,
        alt_cache = sub_args.singularity_cache,  
        threads = int(sub_args.threads),
        jobname = sub_args.job_name,
        submission_script=os.path.join(__home__, 'src', 'run.sh'),
        logger = logfh,
        additional_bind_paths = ",".join(bindpaths),
        tmp_dir = sub_args.tmp_dir, 
    )
    
    # Step 5. Wait for subprocess to complete,
    # this is blocking and not asynchronous
    if not sub_args.silent:
        print("\nRunning {} pipeline in '{}' mode...".format(_name, sub_args.mode)) 
    mjob.wait()
    logfh.close()

    # Step 6. Relay information about submission
    # of the master job or the exit code of the 
    # pipeline that ran in local mode
    if sub_args.mode == 'local':
        if int(mjob.returncode) == 0:
            print('{} pipeline has successfully completed'.format(_name))
        else:
            fatal('{} pipeline failed. Please see {} for more information.'.format(_name,
                os.path.join(sub_args.output, 'logfiles', 'snakemake.log')))
    elif sub_args.mode == 'slurm':
        jobid = open(os.path.join(sub_args.output, 'logfiles', 'mjobid.log')).read().strip()
        if not sub_args.silent:
            if int(mjob.returncode) == 0:
                print('Successfully submitted master job: ', end="")
            else:
                fatal('Error occurred when submitting the master job.')
        print(jobid)


def install(sub_args):
    """Downloads resource bundle locally in parallel chunks.
    Reference files will be pulled from chunks defined in 
    'config/install.json' onto the local filesystem. This
    function is a wrapper to 'src/download.py'. Please see
    that script for more information.
    @param sub_args <parser.parse_args() object>:
        Parsed arguments for unlock sub-command
    """
    print(sub_args)
    # Read in config file for install
    with open(
        os.path.join(__home__, 'config', 'install.json')
    ) as fh:
        install_config = json.load(fh)

    # Try to install any missing targets
    download_links = []
    md5_checksums = []
    for target in install_config['install']:
        download_links = list(install_config['install'][target].keys())
        md5_checksums = list(install_config['install'][target].values())
        # Set missing required options
        # for src/download.py, need to
        # pass links, MD5 checksums and
        # the output directory 
        sub_args.input = download_links
        sub_args.md5 = md5_checksums
        sub_args.output = sub_args.ref_path
        # Pass options to download.py
        installer(sub_args)
    
    # Concatenate the locally 
    # download file chunks to 
    # restore tarball and then
    # extract the archive
    if not sub_args.dry_run:
        for target in install_config['install']:
            # Gather all chunks
            download_links = list(install_config['install'][target].keys())
            local_chunks = [
                os.path.join(sub_args.ref_path, f.split('/')[-1]) 
                for f in download_links
            ]
            # Restore the tarball,
            # TODO: add an option
            # blow away the chunks
            # after the restore
            print('Merging chunks... {0}'.format(','.join(local_chunks)))
            tarball = cat(
                local_chunks, 
                os.path.join(
                    sub_args.ref_path,
                    'merged_chunks.tar.gz'
                )
            )
            # Extract the tarball
            print('Extracting tarball... {0}'.format(tarball))
            unpack_archive(tarball, sub_args.ref_path)


def cache(sub_args):
    """Caches remote resources or reference files stored on DockerHub and S3.
    Local SIFs will be created from images defined in 'config/containers/images.json'.
    @TODO: add option to cache other shared S3 resources (i.e. kraken db and fqscreen indices)
    @param sub_args <parser.parse_args() object>:
        Parsed arguments for unlock sub-command
    """
    print(sub_args)
    # Check for dependencies
    require(['singularity'], ['singularity'])
    sif_cache = sub_args.sif_cache
    # Get absolute PATH to templates in exome-seek git repo
    repo_path = os.path.dirname(os.path.abspath(__file__))
    images = os.path.join(repo_path, 'config','containers.json')

    # Create image cache
    if not exists(sif_cache):
        # Pipeline output directory does not exist on filesystem
        os.makedirs(sif_cache)
    elif exists(sif_cache) and os.path.isfile(sif_cache):
        # Provided Path for pipeline output directory exists as file
        raise OSError("""\n\tFatal: Failed to create provided sif cache directory!
        User provided --sif-cache PATH already exists on the filesystem as a file.
        Please {} cache again with a different --sif-cache PATH.
        """.format(_name)
        )

    # Check if local SIFs already exist on the filesystem
    with open(images, 'r') as fh:
        data = json.load(fh)

    pull = []
    for image, uri in data['images'].items():
        sif = os.path.join(sif_cache, '{}.sif'.format(os.path.basename(uri).replace(':', '_')))
        if not exists(sif):
            # If local sif does not exist on in cache, print warning
            # and default to pulling from URI in config/containers.json
            print('Image will be pulled from "{}".'.format(uri), file=sys.stderr)
            pull.append(uri)

    if not pull:
        # Nothing to do!
        print('Singularity image cache is already up to update!')
    else:
        # There are image(s) that need to be pulled 
        if not sub_args.dry_run:
            # container cache script: src/cache.sh
            # Quote user provided values to avoid shell injections
            username = os.environ.get('USER', os.environ.get('USERNAME'))
            exitcode = bash(
                str(os.path.join(repo_path, 'src', 'cache.sh')) + 
                ' local ' +
                " -s '{}' ".format(sif_cache) +
                " -i '{}' ".format(','.join(pull)) + 
                " -t '{0}/{1}/.singularity/' ".format(sif_cache, username)
            )
            # Check exitcode of caching script 
            if exitcode != 0:
                fatal('Fatal: Failed to pull all containers. Please try again!')
            print('Done: sucessfully pulled all software containers!')


def parsed_arguments(name, description):
    """Parses user-provided command-line arguments. Requires argparse and textwrap
    package. argparse was added to standard lib in python 3.5 and textwrap was added
    in python 3.5. To create custom help formatting for subparsers a docstring is
    used create the help message for required options. argparse does not support named
    subparser groups, which is normally what would be used to accomphish this reformatting.
    As so, the help message for require options must be suppressed. If a new required arg
    is added to a subparser, it must be added to the docstring and the usage statement
    also must be updated.
    @param name <str>:
        Name of the pipeline or command-line tool 
    @param description <str>:
        Short description of pipeline or command-line tool 
    """

    # Create a top-level parser
    c = Colors     # add styled logo
    styled_name = "{}{}{}meta{}virs{}".format(c.bold, c.bg_black, c.white, c.cyan, c.end)
    description = "{}{}{}".format(c.bold, description, c.end)
    parser = argparse.ArgumentParser(description = '{}: {}'.format(styled_name, description))

    # Adding Verison information
    parser.add_argument(
        '--version', 
        action = 'version', 
        version = '%(prog)s {}'.format(__version__)
    )

    # Create sub-command parser
    subparsers = parser.add_subparsers()

    # Sub-parser for the "run" sub-command
    # Grouped sub-parser arguments are currently 
    # not supported: https://bugs.python.org/issue9341
    # Here is a work around to create more useful help message for named
    # options that are required! Please note: if a required arg is added the
    # description below should be updated (i.e. update usage and add new option)
    required_run_options = textwrap.dedent("""\
        {0}: {1}

        {3}Synopsis:{4} Run the pipeline with your raw data.
          $ {2} run [--help] [--aggregate] \\
                         [--mode <slurm,local>] [--job-name JOB_NAME] \\
                         [--dry-run] [--silent] [--sif-cache SIF_CACHE] \\
                         [--singularity-cache SINGULARITY_CACHE] \\
                         [--tmp-dir TMP_DIR] [--threads THREADS] \\
                         [--resource-bundle RESOURCE_BUNDLE] \\
                         --input INPUT [INPUT ...] \\
                         --output OUTPUT

        Optional arguments are shown in square brackets.

        {3}Required arguments:{4}
          --input INPUT [INPUT ...]
                                Input FastQ file(s) to process. The pipeline supports
                                single-end and paired-end data, and it can process a 
                                set of samples containing a mixture of single-end and 
                                pair-end FastQ files simultaneously. Please ensure the
                                input FastQ files are gzipp-ed.
                                  Example: --input .tests/*.R?.fastq.gz
          --output OUTPUT
                                Path to an output directory. This location is where
                                the pipeline will create all of its output files, also
                                known as the pipeline's working directory. If the user
                                provided working directory has not been initialized,
                                it will be created automatically.
                                  Example: --output /data/$USER/output

        {3}Analysis options:{4}
          --aggregate           Aggregate contig annotation results into one mutli-
                                sample, project-level interactive Krona report. By
                                default, any resulting reports will be created at 
                                a per-sample level.
                                  Example: --aggregate

        {3}Orchestration options:{4}
          --mode {{slurm,local}}  
                                Method of execution. Defines the mode of execution. 
                                Vaild options for this mode include: local or slurm. 
                                Additional modes of exection are coming soon, default:  
                                slurm.
                                Here is a brief description of each mode:
                                   local: uses local method of execution. local runs 
                                will run serially on compute instance. This is useful 
                                for testing, debugging, or when a users does not have
                                access to a  high  performance  computing environment.
                                If this option is not provided, it will default to a 
                                slurm mode of execution. 
                                   slurm: uses slurm execution backend. This method 
                                will submit jobs to a  cluster  using sbatch. It is 
                                recommended running the pipeline in this mode as it 
                                will be significantly faster. 
                                  Example: --mode slurm
          --job-name JOB_NAME   
                                Overrides the name of the pipeline's master job. When 
                                submitting the pipeline to a jobscheduler, this option
                                overrides the default name of the master job. This can 
                                be useful for tracking the progress or status of a run, 
                                default: pl:{2}.
                                  Example: --job-name {2}_03-14.1592
          --dry-run             
                                Does not execute anything. Only displays what steps in
                                the pipeline remain or will be run.
                                  Example: --dry-run
          --silent              
                                Silence standard output. This will reduces the amount 
                                of information displayed to standard  output  when the 
                                master job is submitted to the job scheduler. Only the 
                                job id of the master job is returned.
                                  Example: --silent
          --singularity-cache SINGULARITY_CACHE
                                Overrides the $SINGULARITY_CACHEDIR variable. Images
                                from remote registries are cached locally on the file
                                system. By default, the singularity cache is set to:
                                '/path/to/output/directory/.singularity/'. Please note
                                that this cache cannot be shared across users.
                                  Example: --singularity-cache /data/$USER
          --sif-cache SIF_CACHE
                                Path where a local cache of SIFs are stored. This cache
                                can be shared across users if permissions are properly
                                setup. If a SIF does not exist in the SIF cache, the
                                image will be pulled from Dockerhub. The {2} cache
                                sub command can be used to create a local SIF cache. 
                                Please see {2} cache for more information.
                                  Example: --sif-cache /data/$USER/sifs/
          --resource-bundle RESOURCE_BUNDLE
                                Path to a resource bundle downloaded with the install
                                sub command. The resource bundle contains the set of 
                                required reference files for processing any data. The 
                                path provided to this option will be the path to the 
                                {2} directory that was created when running the
                                {2} install sub command.
                                  Example: --resource-bundle /data/$USER/refs/{2}
          --tmp-dir TMP_DIR     
                                Path on the file system for writing temporary output 
                                files. By default, the temporary directory is set to 
                                '/tmp' for cross-platfrom compatibility; however, if 
                                you are running the pipeline on another cluster with
                                a dedicated space for writing temporary file you may 
                                point to that location. On many systems, this location
                                is set to somewhere in /scratch. If you need to inject 
                                variables into this string that should not be expanded, 
                                please quote this options value in single quotes. 
                                  Example: --tmp-dir '/scratch/cluster_scratch/$USER/'
          --threads THREADS     
                                Max number of threads for local processes. It is
                                recommended setting this vaule to the maximum number of
                                CPUs available on the host machine, default: 2.
                                  Example: --threads: 16
        
        {3}Misc Options:{4}
          -h, --help            Show usage information, help message, and exit.
                                  Example: --help
        """.format(styled_name, description, name, c.bold, c.end))

    # Display example usage in epilog
    run_epilog = textwrap.dedent("""\
        {2}Example:{3}
          # Step 1.) Grab an interactive node,
          # do not run on head node!
          srun -N 1 -n 1 --time=8:00:00 --mem=8gb  --cpus-per-task=2 --pty bash
          module purge
          module load singularity snakemake

          # Step 2A.) Dry-run the pipeline
          ./{0} run --input .tests/*.R?.fastq.gz \\
                         --output /data/$USER/output\\
                         --mode slurm \\
                         --aggregate \\
                         --dry-run

          # Step 2B.) Run the {0} pipeline
          # The slurm mode will submit jobs to 
          # the cluster. It is recommended running 
          # the pipeline in this mode.
          ./{0} run --input .tests/*.R?.fastq.gz \\
                         --output /data/$USER/output \\
                         --mode slurm \\
                         --aggregate

        {2}Version:{3}
          {1}
        """.format(name, __version__, c.bold, c.end))

    # Supressing help message of required args 
    # to overcome no sub-parser named groups
    subparser_run = subparsers.add_parser(
        'run',
        help = 'Run the {} pipeline with input files.'.format(name),
        usage = argparse.SUPPRESS,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description = required_run_options,
        epilog  = run_epilog,
        add_help=False
    )

    # Required Arguments
    # Input FastQ files
    subparser_run.add_argument(
        '--input',
        # Check if the file exists and if it is readable
        type = lambda file: permissions(parser, file, os.R_OK),
        required = True,
        nargs = '+',
        help = argparse.SUPPRESS
    )

    # Output Directory (analysis working directory)
    subparser_run.add_argument(
        '--output',
        type = lambda option: os.path.abspath(os.path.expanduser(option)),
        required = True,
        help = argparse.SUPPRESS
    )

    # Optional Arguments
    # Add custom help message
    subparser_run.add_argument(
        '-h', '--help', 
        action='help', 
        help=argparse.SUPPRESS
    )

    # Analysis options
    # Aggregate results into one report
    subparser_run.add_argument(
        '--aggregate',
        action = 'store_true',
        required = False,
        default = False,
        help = argparse.SUPPRESS
    )

    # Execution Method, run locally 
    # on a compute node, submit to 
    # SLURM job scheduler, etc.
    subparser_run.add_argument(
        '--mode',
        type = str,
        required = False,
        default = "slurm",
        choices = ['slurm', 'local'],
        help = argparse.SUPPRESS
    )

    # Name of master job
    subparser_run.add_argument(
        '--job-name',
        type = str,
        required = False,
        default = 'pl:{}'.format(name),
        help = argparse.SUPPRESS
    )

    # Dry-run (do not execute the workflow, prints what steps remain)
    subparser_run.add_argument(
        '--dry-run',
        action = 'store_true',
        required = False,
        default = False,
        help = argparse.SUPPRESS
    )

    # Silent output mode
    subparser_run.add_argument(
        '--silent',
        action = 'store_true',
        required = False,
        default = False,
        help = argparse.SUPPRESS
    )
    
    # Singularity cache directory (default uses output directory)
    subparser_run.add_argument(
        '--singularity-cache',
        type = lambda option: check_cache(parser, os.path.abspath(os.path.expanduser(option))),
        required = False,
        help = argparse.SUPPRESS
    )
    
    # Local SIF cache directory (default pull from Dockerhub) 
    subparser_run.add_argument(
        '--sif-cache',
        type = lambda option: os.path.abspath(os.path.expanduser(option)),
        required = False,
        help = argparse.SUPPRESS
    )

    # Output Directory of downloaded resource bundle, see 
    # the install sub command for more information on how
    # to download any required references files locally.
    subparser_run.add_argument(
        '--resource-bundle',
        type = lambda option: os.path.abspath(os.path.expanduser(option)),
        required = False,
        default = None,
        help = argparse.SUPPRESS
    )

    # Base directory to write temporary files 
    subparser_run.add_argument(
        '--tmp-dir',
        type = str,
        required = False,
        default = '/tmp/',
        help = argparse.SUPPRESS
    )

    # Number of threads for the pipeline's main proceess
    # This is only applicable for local rules or when the 
    # pipeline is running in local mode.
    subparser_run.add_argument(
        '--threads',
        type = int,
        required = False,
        default = 2,
       help = argparse.SUPPRESS
    )

    # Sub-parser for the "unlock" sub-command
    # Grouped sub-parser arguments are currently 
    # not supported: https://bugs.python.org/issue9341
    # Here is a work around to create more useful help message for named
    # options that are required! Please note: if a required arg is added the
    # description below should be updated (i.e. update usage and add new option)
    required_unlock_options = textwrap.dedent("""\
        {0}: {1}

        {3}Synopsis:{4} Unlock the pipeline's output directory.
          $ {2} unlock [-h] --output OUTPUT

        If the pipeline fails ungracefully, it maybe required to unlock the working
        directory before proceeding again. Please verify that the pipeline is not
        running before running this command. If the pipeline is still running, the
        workflow manager will report the working directory is locked. This is normal
        behavior. Do NOT run this command if the pipeline is still running.

        {3}Required arguments:{4}
          --output OUTPUT
                                Path to a previous run's output directory to
                                unlock. This will remove a lock on the working 
                                directory. Please verify that the pipeline is 
                                not running before running this command.
                                  Example: --output /data/$USER/output

        {3}Misc Options:{4}
          -h, --help            Show usage information, help message, and exit.
                                  Example: --help

        """.format(styled_name, description, name, c.bold, c.end))

    # Display example usage in epilog
    unlock_epilog = textwrap.dedent("""\
        {2}Example:{3}
          # Unlock output directory of pipeline
          {0} unlock --output /scratch/$USER/output

        {2}Version:{3}
          {1}
        """.format(name, __version__, c.bold, c.end))

    # Supressing help message of required args 
    # to overcome no sub-parser named groups
    subparser_unlock = subparsers.add_parser(
        'unlock',
        help = 'Unlocks a previous runs output directory.',
        usage = argparse.SUPPRESS,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description = required_unlock_options,
        epilog = unlock_epilog,
        add_help = False
    )

    # Required Arguments
    # Output Directory (analysis working directory)
    subparser_unlock.add_argument(
        '--output',
        type = str,
        required = True,
        help = argparse.SUPPRESS
    )

    # Add custom help message
    subparser_unlock.add_argument(
        '-h', '--help', 
        action='help', 
        help=argparse.SUPPRESS
    )

    # Sub-parser for the "install" sub-command
    # Grouped sub-parser arguments are 
    # not supported: https://bugs.python.org/issue9341
    # Here is a work around to create more useful help message for named
    # options that are required! Please note: if a required arg is added the
    # description below should be updated (i.e. update usage and add new option)
    required_install_options = textwrap.dedent("""\
        {0}: {1}

        {3}Synopsis:{4} Download the resource bundle locally.
          $ {2} install [-h] [--dry-run] \\
                  [--force] [--threads] \\
                   --ref-path REF_PATH

        The pipeline uses a set of reference files to process the data. 
        These reference files are required and need to be available on 
        the local file system prior to execution. This command can be 
        used to download any required reference files of the pipeline. 
        
        Since most resource bundles are very large; we recommend using 
        multiple threads for pulling reference files concurrently. The 
        resource bundle can be very large so please ensure you have 
        sufficent disk space prior to the download.

        {3}Please Note:{4} 
          The resource bundle is large and requires about 750 GB of 
        available disk space. If you are running the pipeline on the 
        Biowulf cluster, you do NOT need to download the pipeline's
        resource bundle. It is already accessible to all HPC users.

        {3}Required arguments:{4}
          --ref-path REF_PATH
                                Path where the resource bundle will be
                                downloaded. Any resouces defined in the
                                'config/install.json' will be pulled on
                                to the local filesystem. After the files
                                have been downloaded, a new directory 
                                with the name {2} will be created. 
                                It contains all the required reference 
                                files of the pipeline. The path to this 
                                new directory can be passed to the run
                                sub command's --resource-bundle option.
                                Please see the run sub command for more 
                                information.
                                  Example: --ref-path /data/$USER/refs
        
        {3}Orchestration options:{4}
          --dry-run             Does not execute anything. Only displays 
                                what remote resources would be pulled.
                                  Example: --dry-run
        
          --force               Force downloads all files. By default, any
                                files that do not exist locally are pulled;
                                however if a previous instance of an install 
                                did not exit gracefully, it may be necessary 
                                to forcefully re-download all the files.
                                  Example: --force

          --threads THREADS     Number of threads to use for concurrent file 
                                downloads, default: 2.
                                  Example: --threads 12

        {3}Misc Options:{4}
          -h, --help            Show usage information, help message, 
                                and exits.
                                  Example: --help

        """.format(styled_name, description, name, c.bold, c.end))

    # Display example usage in epilog
    install_epilog = textwrap.dedent("""\
        {2}Example:{3}
          # Dry-run download of the resource bundle
          {0} install --ref-path /data/$USER/metavirs/ref \\
                     --force \\
                     --dry-run \\
                     --threads 12

          # Download the resource bundle
          {0} install --ref-path /data/$USER/metavirs/ref \\
                     --force \\
                     --threads 12

        {2}Version:{3}
          {1}
        """.format(name, __version__, c.bold, c.end))

    # Supressing help message of required args 
    # to overcome no sub-parser named groups
    subparser_install = subparsers.add_parser(
        'install',
        help = 'Download reference files locally.',
        usage = argparse.SUPPRESS,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description = required_install_options,
        epilog = install_epilog,
        add_help = False
    )

    # Required Arguments
    # Output Directory where file will be downloaded
    subparser_install.add_argument(
        '--ref-path',
        type = lambda option: os.path.abspath(os.path.expanduser(option)),
        required = True,
        help = argparse.SUPPRESS
    )

    # Optional Arguments
    # Dry-run install command,
    # does not pull any remote resources,
    # just shows what will be pulled 
    subparser_install.add_argument(
        '--dry-run',
        action = 'store_true',
        required = False,
        default = False,
        help=argparse.SUPPRESS
    )

    # Forces downloading of all files
    subparser_install.add_argument(
        '--force',
        action = 'store_true',
        required = False,
        default = False,
        help=argparse.SUPPRESS
    )

    # Number of threads for concurrent downloads
    subparser_install.add_argument(
        '--threads',
        type = int,
        required = False,
        default = 2,
       help = argparse.SUPPRESS
    )

    # Add custom help message
    subparser_install.add_argument(
        '-h', '--help', 
        action='help', 
        help=argparse.SUPPRESS
    )

    # Sub-parser for the "cache" sub-command
    # Grouped sub-parser arguments are 
    # not supported: https://bugs.python.org/issue9341
    # Here is a work around to create more useful help message for named
    # options that are required! Please note: if a required arg is added the
    # description below should be updated (i.e. update usage and add new option)
    required_cache_options = textwrap.dedent("""\
        {0}: {1}

        {3}Synopsis:{4} Cache software containers locally.
          $ {2} cache [-h] [--dry-run] \\
                  --sif-cache SIF_CACHE

        Create a local cache of software dependencies hosted on DockerHub.
        These containers are normally pulled onto the filesystem when the
        pipeline runs; however, due to network issues or DockerHub pull
        rate limits, it may make sense to pull the resources once so a
        shared cache can be created. It is worth noting that a singularity
        cache cannot normally be shared across users. Singularity strictly
        enforces that a cache is owned by the user. To get around this
        issue, the cache subcommand can be used to create local SIFs on
        the filesystem from images on DockerHub.

        {3}Required arguments:{4}
          --sif-cache SIF_CACHE
                                Path where a local cache of SIFs will be 
                                stored. Images defined in containers.json
                                will be pulled into the local filesystem. 
                                The path provided to this option can be 
                                passed to the --sif-cache option of the
                                run sub command. Please see {2} run 
                                sub command for more information.
                                  Example: --sif-cache /data/$USER/cache
        
        {3}Orchestration options:{4}
          --dry-run             Does not execute anything. Only displays 
                                what remote resources would be pulled.
                                  Example: --dry-run

        {3}Misc Options:{4}
          -h, --help            Show usage information, help message, 
                                and exits.
                                  Example: --help

        """.format(styled_name, description, name, c.bold, c.end))

    # Display example usage in epilog
    cache_epilog = textwrap.dedent("""\
        {2}Example:{3}
          # Cache software containers of pipeline
          {0} cache --sif-cache /data/$USER/cache

        {2}Version:{3}
          {1}
        """.format(name, __version__, c.bold, c.end))

    # Supressing help message of required args 
    # to overcome no sub-parser named groups
    subparser_cache = subparsers.add_parser(
        'cache',
        help = 'Cache software containers locally.',
        usage = argparse.SUPPRESS,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description = required_cache_options,
        epilog = cache_epilog,
        add_help = False
    )

    # Required Arguments
    # Output Directory (analysis working directory)
    subparser_cache.add_argument(
        '--sif-cache',
        type = lambda option: os.path.abspath(os.path.expanduser(option)),
        required = True,
        help = argparse.SUPPRESS
    )

    # Optional Arguments
    # Dry-run cache command (do not pull any remote resources)
    subparser_cache.add_argument(
        '--dry-run',
        action = 'store_true',
        required = False,
        default = False,
        help=argparse.SUPPRESS
    )

    # Add custom help message
    subparser_cache.add_argument(
        '-h', '--help', 
        action='help', 
        help=argparse.SUPPRESS
    )

    # Define handlers for each sub-parser
    subparser_run.set_defaults(func = run)
    subparser_unlock.set_defaults(func = unlock)
    subparser_install.set_defaults(func = install)
    subparser_cache.set_defaults(func = cache)

    # Parse command-line args
    args = parser.parse_args()
    return args


def main():
    # Check for any usage
    if len(sys.argv) == 1:
        fatal('Invalid usage: {} [-h] [--version] ...'.format(_name))

    # Collect args for sub-command
    args = parsed_arguments(
        name = _name,
        description = _description
    )

    # Display version information
    err('{} ({})'.format(_name, __version__))

    # Mediator method to call sub-command's set handler function
    args.func(args)


if __name__ == '__main__':
    main()