#!/usr/bin/env python3
#
import argparse
from   datetime   import datetime, timezone
from   glob       import glob
import os
from   os         import path
import re
import subprocess
import sys

if __name__ == '__main__':
    sys.path.append( path.abspath( path.join( path.dirname( path.realpath(__file__)), '..', '..' )))

import palm
import palm.config
import palm.system


LOG = palm.logger(__name__)


def main( prog=None, argv=None ):

    # palm-cli build [-h]          [<palmbuild-option>] --bash
    # palm-cli build [-h]          [<palmbuild-option>] [<cli-options>]
    # palm-cli build [-h] binaries [<palmbuild-option>] [<cli-options>]
    #
    result = 0

    try:
        opt, palm_argv, cli_argv = parse_option( prog, argv )
        if opt.get('help'):
            build_argparse(prog).print_help()

        else:
            palm.init_logging(opt.get('log_append'))

            # process configuration file ...
            #
            yamlfile = palm.system.yaml_config_file(opt.get('configuration_identifier'))

            assert path.isfile(yamlfile),            \
                '\n  +++ YAML configuration file: '+ \
                '\n         ' + yamlfile           + \
                '\n      does not exist'

            yamldefs, palmdefs = palm.config.yaml_process( yamlfile, opt )

            palmdefs.yamlfile            = yamlfile
            palmdefs.palm_version_string = palm.system.version()
            palmdefs.palm_cli_options    = ' '.join(cli_argv)
            palmdefs.palm_options        = ' '.join(palm_argv)
            palmdefs.verbose             = '' if palmdefs.silent else '-v'

            LOG.debug( palmdefs.expand( 'cli options:  ${palm_cli_options}' ))
            LOG.debug( palmdefs.expand( 'palm options: ${palm_options}'     ))

            # ... and start building ...
            #
            if palmdefs.use_bash_script:

                # ... write to palm configuration file ...
                #
                palm.config.yaml_to_palm(
                    palmdefs,
                    palmdefs.config_file
                    )

                # ... and call script
                #
                result = palm.system.run( 'palmbuild', palm_argv )

            elif palmdefs.binaries_only:
                result = build_binaries(palmdefs)

            elif palmdefs.run_identifier:
                result = build_run_catalog(palmdefs)

            else:
                result = build_depository(palmdefs)

        if not palmdefs.silent:
            LOG.info('')
            LOG.info(' --> palm-cli build finished')
            LOG.info('')

    except KeyboardInterrupt:
        LOG.info('')
        LOG.info('+++ palm-cli build killed by "^C"')
        LOG.info('')

        result = 2

    except Exception as exception:
        LOG.error('')
        LOG.error('+++ palm-cli build crashed')

        messages = str(exception).split('\n')
        for message in messages:
            LOG.error(message)

        if palm.traceback:
            LOG.error( '', exc_info=True )

        result = 1

    return result


def build_depository( defs ):

    ###########################################################################
    # useful helper
    #
    def ASSERT( condition, message ):
        assert condition, defs.expand( message )
    def EXP( string, context=None ):
        return defs.expand(string, context=context)
    def ENV( string, context=os.environ ):
        return defs.expand(string, context=context)
    def ENV_EVAL( name, context=os.environ ):
        return defs.eval( name, context=context)
    def LOG_info( message ):
        LOG.info( EXP(message) )
    def palm_shell( command, capture_output=False, ignore_error=False ):
        return palm.system.shell( EXP(command), None, capture_output, ignore_error )
    def palm_ssh( command, capture_output=False, ignore_error=False ):
        return palm.system.shell(
            EXP('ssh -q ${remote_key} ${remote_port_ssh} ${remote_user} \'"\'"\' %s \'"\'"\'' % command),
            capture_output=capture_output,
            ignore_error=ignore_error
            )
    def palm_scp( files, ignore_error=False ):
        if not defs.remote_env:
            defs.remote_env = dict( tuple(line.strip().split('=',1)) for line in palm_ssh('env', capture_output=True).splitlines())
        return palm.system.shell(
            ENV( EXP('scp ${remote_key} ${remote_port_scp} %s' % files), defs.remote_env),
            ignore_error=ignore_error
            )
    def palm_chdir( path ):
        return palm.system.chdir( EXP(path), None )
    def palm_mkdir( directory, titel='' ):
        directory = EXP(directory)
        if not path.isdir( directory ):
            try:
                palm_shell('mkdir -p ' + directory )
                if not defs.silent:
                    LOG.info('')
                    LOG.info('  *** directory '+ titel  )
                    LOG.info('         ' + directory    )
                    LOG.info('      was created'        )

            except:
                LOG.error('')
                LOG.error('  +++ directory ' + titel )
                LOG.error('         ' + directory    )
                LOG.error('      cannot be created'  )
                raise

    ###########################################################################
    # REMEMBER CURRENT DIRECTORY
    #
    defs.path_build_depository_enter = os.getcwd()

    try:
        # CHECK CONFIGURATION IDENTIFIER
        #
        ASSERT(
            defs.configuration_identifier,
            '\n  +++ missing configuration identifier, call with "-c <ID>"'
            )

        # CHECK, IF THE BASE DIRECTORY PATH HAS BEEN GIVEN
        #
        ASSERT(
            defs.path_base,
            '\n  +++ no base directory found in configuration file'
            )

        defs.path_base_local = ENV_EVAL('path_base')
        ASSERT(
            path.isdir( defs.path_base_local ),
            '\n  +++ base directory "${path_base_local}" '\
            '\n      does not exist'
            )

        # CHECK, IF THE SOURCE DIRECTORY PATH HAS BEEN GIVEN
        #
        if not defs.source_palm:
            defs.source_palm = palm.system.source_dir()

        ASSERT(
            defs.source_palm,
            '\n  +++ no source directory found in configuration file'
            )

        defs.source_palm = ENV_EVAL('source_palm')
        ASSERT(
            path.isdir( defs.source_palm ),
            '\n  +++ source directory "${source_palm}"'\
            '\n      does not exist'
            )

        # DECLARATION OF VARIABLES
        #
        defs.program_name      = 'palm'
        defs.suf               = 'f90'
        defs.tar_file_sources  = EXP('${program_name}_sources.tar')
        defs.tar_file_binaries = EXP('${program_name}_current_version.tar')

        defs.make_depository = EXP('MAKE_DEPOSITORY_${configuration_identifier}')
        defs.path_depository = path.join(
            defs.path_base_local,
            defs.make_depository
            )

        # SHOW PARAMETER SUMMARY
        #
        if defs.show_summary:
            build_summary( defs )
            while True:
                answer = LOG.input('>>> [c]ontinue / [a]bort? ').lower()
                if answer == 'a' or answer == 'abort':
                    return
                if answer == 'c' or answer == 'continue' or answer == 'y' or answer == 'yes' or answer == 's':
                    break

        # TAR THE SOURCES AND MAKEFILES
        #
        if not defs.silent:
            LOG_info('')
            LOG_info('  *** tar of makefile and source files in')
            LOG_info('      ${source_palm}'                     )

        palm_chdir('${source_palm}')
        palm_shell('tar -cf ${tar_file_sources} Makefile *.${suf}')


        if not defs.host_remote_ip:

            # CREATE DEPOSITORY, IF IT DOES NOT EXIST
            #
            if not path.isdir( defs.path_depository ):
                palm_mkdir('${path_depository}',' for make depository:')

            # MAKE BINARIES ON LOCALE HOST ...
            #
            if not defs.silent:
                LOG_info('')
                LOG_info('  *** untar current source on local host in')
                LOG_info('      ${path_depository}'                   )

            palm_chdir('${path_depository}')
            palm_shell(
                # FAIL ON FIRST ERROR
                #
                'set -e\n'

                # FIXME: in contrast to remote build, bash script 'palmbuild'
                #        does not use existing 'palm_current_version.tar'
                #        for building local depository (!?)

                # UNTAR PREVIOUS UPDATE ON REMOTE HOST, IF EXISTING
                #
                # 'if [[ -f ${tar_file_binaries} ]]\n'
                # 'then\n'
                # '  [[ "${verbose}" ]] && printf "\n  *** untar previous update on local host\n"\n'
                # '  tar -xf  ${tar_file_binaries}  2>&1\n'
                # 'fi\n'

                # UNTAR CURRENT SOURCES ON REMOTE HOST
                #
                '[[ "${verbose}" ]] && printf "\n  *** untar current sources on local host\n"\n'
                'cp ${source_palm}/${tar_file_sources} .\n'
                'tar xf ${tar_file_sources}'
                )

            build_binaries(defs)

            # TAR NEW VERSION ON LOCAL HOST
            #
            if not defs.silent:
                LOG_info('')
                LOG_info('  *** tar update on local host ...')

            palm_shell(
                'tar -cf ${tar_file_binaries} '
                    '${program_name} '
                    '*.${suf} '
                    '*.o '
                    '*.mod '
                    '*.x'
                )

        else:
            # CHECK SETTINGS IN CASE OF COMPILING ON REMOTE MACHINE
            #
            ASSERT(
                defs.host_remote_username,
                '\n  +++ no remote user name given in configuration file'
                )

            # SOME SHORTCUTS ...
            #
            defs.remote_user       = EXP('${host_remote_username}@${host_remote_ip}')
            defs.remote_key        = ENV(EXP('-i $HOME/.ssh/${ssh_key}')) if defs.ssh_key else ''
            defs.remote_port_ssh   = EXP('-p ${scp_port}') if defs.scp_port else ''
            defs.remote_port_scp   = EXP('-P ${scp_port}') if defs.scp_port else ''
            defs.remote_yamlname   = path.basename(defs.yamlfile)
            defs.remote_depository = path.join(
                defs.path_base,
                defs.make_depository
                )

            # UPDATE PALM-CLI ON REMOTE HOST (IF REQUIRED)
            #
            if defs.host_remote_cli_update:
                build_cli_copy(defs)

            # COPY SOURCES ARCHIVE TO REMOTE HOST
            #
            if not defs.silent:
                LOG_info('')
                LOG_info('  *** copying "${tar_file_sources}" to "${host_remote_ip}:${remote_depository}')

            palm_ssh(
                '\n'
                # CREATE SOURCE-CODE DIRECTORY, IF IT DOES NOT EXIST
                #
                '  if [[ ! -d "${remote_depository}" ]]\n'
                '  then\n'
                '    [[ "${verbose}" ]] && printf "\\n  *** ${remote_depository} will be created\\n"\n'
                '    mkdir -p  ${remote_depository}\n'
                '  fi\n'
                )
            palm_scp(
                '${source_palm}/${tar_file_sources} '
                '${yamlfile} '
                '${remote_user}:${remote_depository} '
                )

            # BUILD ON REMOTE HOST
            #
            try:
                palm_ssh(
                    '\n'

                    # FAIL ON FIRST ERROR
                    #
                    '   set -e\n'

                    # UNTAR PREVIOUS UPDATE ON REMOTE HOST, IF EXISTING
                    #
                    '   cd ${remote_depository}\n'
                    '   if [[ -f ${tar_file_binaries} ]]\n'
                    '   then\n'
                    '      [[ "${verbose}" ]] && printf "\\n  *** untar previous update on remote host\\n"\n'
                    '      tar -xf  ${tar_file_binaries}  2>&1\n'
                    '   fi\n'

                    # UNTAR CURRENT SOURCES ON REMOTE HOST
                    #
                    '   [[ "${verbose}" ]] && printf "\\n  *** untar current sources on remote host\\n"\n'
                    '   tar -xf ${tar_file_sources}\n'

                    # CALL PALM-CLI ON REMOTE HOST
                    #
                    '   [[ "${verbose}" ]] && printf "\\n  *** start PALM command-line-interface on remote host\\n"\n'
                    '   ln -fs ${host_remote_cli_path}/palm-cli palm-cli\n'
                    '   ./palm-cli build binaries '
                            '--pref ${remote_yamlname} '
                            '--no-summary ' # don't show summary on remote host
                            '${palm_options} '
                            '${palm_cli_options} '
                            '\n'

                    # TAR NEW VERSION ON REMOTE HOST
                    #
                    '   [[ "${verbose}" ]] && printf "\\n  *** tar update on remote host ...\\n"\n'
                    '   chmod u+w *\n'
                    '   tar -cf ${tar_file_binaries} '
                            '${program_name} '
                            '*.${suf} '
                            '*.o '
                            '*.mod '
                            '*.x '
                            'palm-cli '
                            '\n'
                    )
            finally:
                # FETCH LOG-FILE FROM REMOTE HOST ...
                #
                palm_scp(
                    '${remote_user}:${remote_depository}/palm-cli.log '
                    '${path_build_depository_enter}/palm-cli.${host_remote_ip}.log '
                    )

        return 0

    finally:
        palm_chdir('${path_build_depository_enter}')


def build_run_catalog( defs ):

    ###########################################################################
    # useful helper
    #
    def ASSERT( condition, message ):
        assert condition, defs.expand( message )
    def EXP( string, context=None ):
        return defs.expand(string, context=context)
    def ENV( string, context=os.environ ):
        return defs.expand(string, context=context)
    def ENV_EVAL( name, context=os.environ ):
        return defs.eval( name, context=context)
    def LOG_info( message ):
        LOG.info( EXP(message) )
    def LOG_error( message ):
        LOG.error( EXP(message) )
    def palm_shell( command, capture_output=False, ignore_error=False ):
        return palm.system.shell( EXP(command), None, capture_output, ignore_error )
    def palm_ssh( command, capture_output=False, ignore_error=False ):
        return palm.system.shell(
            EXP('ssh -q ${remote_key} ${remote_port_ssh} ${remote_user} \'"\'"\' %s \'"\'"\'' % command),
            capture_output=capture_output,
            ignore_error=ignore_error
            )
    def palm_scp( files, ignore_error=False ):
        if not defs.remote_env:
            defs.remote_env = dict( tuple(line.strip().split('=',1)) for line in palm_ssh('env', capture_output=True).splitlines())
        return palm.system.shell(
            ENV( EXP('scp ${remote_key} ${remote_port_scp} %s' % files), defs.remote_env),
            ignore_error=ignore_error
            )
    def palm_chdir( path ):
        return palm.system.chdir( EXP(path), None )
    def palm_glob( pathname ):
        return glob( EXP(pathname) )

    ###########################################################################
    # REMEMBER CURRENT DIRECTORY
    #
    defs.path_build_run_catalog_enter = os.getcwd()

    try:
        # CHECK CONFIGURATION IDENTIFIER
        #
        ASSERT(
            defs.configuration_identifier,
            '\n  +++ missing configuration identifier, call with "-c <ID>"'
            )

        # CHECK RUN IDENTIFIER
        #
        ASSERT(
            defs.run_identifier,
            '\n  +++ missing run identifier, call with "-r <NAME>"'
            )

        # CHECK, IF THE BASE DIRECTORY PATH HAS BEEN GIVEN
        #
        ASSERT(
            defs.path_base,
            '\n  +++ no base directory found in configuration file'
            )

        defs.path_base_local = ENV_EVAL('path_base')
        ASSERT(
            path.isdir( defs.path_base_local ),
            '\n  +++ base directory "${path_base_local}" '\
            '\n      does not exist'
            )

        # CHECK, IF THE SOURCE FOR RUN DIRECTORY EXISTS
        #
        defs.sources_for_run_catalog = EXP('SOURCES_FOR_RUN_${configuration_identifier}_${run_identifier}')

        defs.path_source_catalog = path.join(
            defs.path_base_local,
            defs.sources_for_run_catalog
            )

        ASSERT(
            path.isdir( defs.path_source_catalog ),
            '\n  +++ source catalog "${path_source_catalog}" '\
            '\n      does not exist'
            )

        # CHECK, IF THE TEMPORARY DIRECTORY PATH HAS BEEN GIVEN
        #
        ASSERT(
            defs.fast_io_catalog,
            '\n  +++ no temporary directory found in configuration file'
            )

        # DECLARATION OF VARIABLES
        #
        defs.program_name      = 'palm'
        defs.suf               = 'f90'
        defs.tar_file_sources  = EXP('${program_name}_sources.tar')
        defs.tar_file_binaries = EXP('${program_name}_current_version.tar')

        defs.make_depository = EXP('MAKE_DEPOSITORY_${configuration_identifier}')
        defs.path_depository = path.join(
            defs.path_base_local,
            defs.make_depository
            )

        # SHOW PARAMETER SUMMARY
        #
        if defs.show_summary:
            build_summary( defs )
            while True:
                answer = LOG.input('>>> [c]ontinue / [a]bort? ').lower()
                if answer == 'a' or answer == 'abort':
                    return
                if answer == 'c' or answer == 'continue' or answer == 'y' or answer == 'yes' or answer == 's':
                    break

        if not defs.host_remote_ip:

            # FIRST CHECK, IF COMPILED SOURCES FOR THIS RUN IDENTIFIER EXISTS
            # AND ASK, IF THEY SHALL BE USED
            #
            defs.fast_io_catalog = ENV_EVAL('fast_io_catalog')
            ASSERT(
                path.isdir( defs.fast_io_catalog ),
                '\n  +++ temporary directory "${fast_io_catalog}" '\
                '\n      does not exist'
                )

            defs.path_run_catalog = path.join(
                defs.fast_io_catalog,
                defs.sources_for_run_catalog
                )

            if defs.use_existing_sources_folder and path.isdir( defs.path_run_catalog ):
                LOG_info('')
                LOG_info('  *** compiled sources for run "${run_identifier}" found in folder')
                LOG_info('         ${path_run_catalog}' )
                LOG_info('      will be used!'       )
                return 0

            # SECOND CHECK, IF A DEPOSITORY EXISTS ON THE LOCAL MACHINE
            #
            ASSERT(
                path.isdir( defs.path_depository ),
                '\n  +++ directory for local make depository:'\
                '\n         ${path_depository}' \
                '\n      not found. Please run "palm-cli build -c ${configuration_identifier}" before'
                )

            # COPY MAKE DEPOSITORY ON LOCAL MACHINE TO SOURCES_FOR_RUN_...
            #
            if not defs.silent:
                LOG_info('')
                LOG_info('  *** copy ${make_depository} on local host to')
                LOG_info('      ${path_run_catalog}')

            try:
                palm_shell(
                    'rm -rf ${path_run_catalog}\n'
                    'mkdir -p ${path_run_catalog}'
                    )

            except:
                LOG_error('')
                LOG_error('  +++ SOURCES_FOR_RUN catalog cannot be created.')
                LOG_error('      Check setting for fast io catalog in your local file.')
                raise

            palm_shell('cp ${path_depository}/${tar_file_binaries} ${path_run_catalog}')
            palm_chdir('${path_run_catalog}')
            palm_shell('tar -xf ${tar_file_binaries}' )
            # BUGFIX: copy palm-cli into path_run_catalog
            palm_shell('cp -r ' + str(sys.path[0]) + ' ./bin')

            # COPY CONTENTS OF SOURCES_FOR_RUN_... TO SOURCES_FOR_RUN_...
            # IN THE FAST_IO_CATALOG ON THE LOCAL MACHINE
            #
            if not defs.silent:
                LOG_info('')
                LOG_info('  *** copy ${path_source_catalog} to'   )
                LOG_info('      ${path_run_catalog} on local host')

            palm_shell('cp ${path_source_catalog}/{*,.[!.]*} ${path_run_catalog}')

            # CREATE EXECUTABLE FROM THE NEW/MODIFIED SOURCE FILES, IF THERE ARE ANY
            #
            if palm_glob('${path_source_catalog}/*.${suf}'):
                try:
                    build_binaries(defs)
                except:
                    palm_shell('rm -rf ${path_run_catalog}')
                    raise

            else:
                LOG_info('')
                LOG_info('  *** nothing to compile for this run')

        else:
            # CHECK SETTINGS IN CASE OF COMPILING ON REMOTE MACHINE
            #
            ASSERT(
                defs.host_remote_username,
                '\n  +++ no remote user name given in configuration file'
                )

            # SOME SHORTCUTS ...
            #
            defs.remote_user       = EXP('${host_remote_username}@${host_remote_ip}')
            defs.remote_key        = ENV(EXP('-i $HOME/.ssh/${ssh_key}')) if defs.ssh_key else ''
            defs.remote_port_ssh   = EXP('-p ${scp_port}') if defs.scp_port else ''
            defs.remote_port_scp   = EXP('-P ${scp_port}') if defs.scp_port else ''
            defs.remote_yamlname   = path.basename(defs.yamlfile)
            defs.remote_depository = path.join(
                defs.path_base,
                defs.make_depository
                )
            defs.remote_run_catalog = path.join(
                defs.fast_io_catalog,
                defs.sources_for_run_catalog
                )

            # FIRST CHECK, IF COMPILED SOURCES FOR THIS RUN IDENTIFIER EXISTS
            # AND ASK, IF THEY SHALL BE USED
            #
            if defs.use_existing_sources_folder:
                found = palm_ssh(
                    '[[ ! -d ${remote_run_catalog} ]] || echo "sources for run found"',
                    capture_output=True
                    )

                if 'sources for run found' in found:
                    LOG_info('')
                    LOG_info('  *** compiled sources for run "${run_identifier}" found on remote host in folder')
                    LOG_info('         ${remote_run_catalog}' )
                    LOG_info('      will be used!'            )
                    return 0

            # UPDATE PALM-CLI ON REMOTE HOST (IF REQUIRED)
            #
            if defs.host_remote_cli_update:
                build_cli_copy(defs)

            # COPY MAKE DEPOSITORY ON REMOTE MACHINE TO SOURCES_FOR_RUN_...
            #
            if not defs.silent:
                LOG_info('')
                LOG_info('  *** copy ${make_depository} on remote host to ')
                LOG_info('      ${remote_run_catalog}'                     )

            try:
                palm_ssh(
                    '\n'

                    # FAIL ON FIRST ERROR
                    #
                    '   set -e \n'

                    # (RE)CREATE REMOTE SOURCE_FOR_RUN_ ...
                    #
                    '   rm -rf ${remote_run_catalog}\n'
                    '   mkdir -p ${remote_run_catalog}\n'

                    # UNTAR REMOTE DEPOSITORY
                    #
                    '   cp ${remote_depository}/${tar_file_binaries} ${remote_run_catalog}\n'
                    '    cd ${remote_run_catalog}\n'
                    '   tar xf ${tar_file_binaries}\n'
                    )
            except:
                LOG_error('')
                LOG_error('  +++ SOURCES_FOR_RUN catalog cannot be created.')
                LOG_error('      Check setting in your config file.')
                raise

            # COPY CONTENTS OF SOURCES_FOR_RUN_... TO SOURCES_FOR_RUN_... ON THE REMOTE MACHINE
            #
            if not defs.silent:
                LOG_info('')
                LOG_info('  *** copy ${path_source_catalog}')
                LOG_info('      to $sources_for_run_catalog on remote host')

            palm_scp(
                '${path_source_catalog}/{*,.[!.]*} '
                '${yamlfile} '
                '${remote_user}:${remote_run_catalog} '
                )

            # CREATE EXECUTABLE FROM THE NEW/MODIFIED SOURCE FILES, IF THERE ARE ANY
            #
            if palm_glob('${path_source_catalog}/*.${suf}'):
                try:
                    palm_ssh(
                        '\n'

                        # CALL PALM-CLI ON REMOTE HOST
                        #
                        '   [[ \\"${verbose}\\" ]] && printf \\"\\n  *** start PALM command-line-interface on remote host\\n\\"\n'
                        '    cd ${remote_run_catalog}\n'
                        '   ./palm-cli build binaries '
                                '--pref ${remote_yamlname} '
                                '--no-summary ' # don't show summary on remote host
                                '${palm_options} '
                                '${palm_cli_options} '
                                '\n'
                        )
                finally:
                    # FETCH LOG-FILE FROM REMOTE HOST ...
                    #
                    palm_scp(
                        '${remote_user}:${remote_run_catalog}/palm-cli.log '
                        '${path_build_run_catalog_enter}/palm-cli.${host_remote_ip}.log '
                        )

            else:
                LOG_info('')
                LOG_info('  *** nothing to compile for this run')

        return 0

    finally:
        palm_chdir('${path_build_run_catalog_enter}')


def build_binaries( defs ):

    ###########################################################################
    # useful helper
    #
    def ASSERT( condition, message ):
        assert condition, defs.expand( message )
    def EXP( string, context=None ):
        return defs.expand(string, context=context)
    def ENV( string, context=os.environ ):
        return defs.expand(string, context=context)
    def ENV_EVAL( name, context=os.environ ):
        return defs.eval( name, context=context)
    def LOG_info( message ):
        LOG.info( EXP(message) )
    def LOG_error( message ):
        LOG.error( EXP(message) )
    def palm_shell( command, capture_output=False, ignore_error=False ):
        return palm.system.shell( EXP(command), None, capture_output, ignore_error )
    def palm_ssh( command, capture_output=False, ignore_error=False ):
        return palm.system.shell(
            EXP('ssh -q ${remote_key} ${remote_port_ssh} ${remote_user} \'"\'"\' %s \'"\'"\'' % command),
            capture_output=capture_output,
            ignore_error=ignore_error
            )
    def palm_scp( files, ignore_error=False ):
        if not defs.remote_env:
            defs.remote_env = dict( tuple(line.strip().split('=',1)) for line in palm_ssh('env', capture_output=True).splitlines())
        return palm.system.shell(
            ENV( EXP('scp ${remote_key} ${remote_port_scp} %s' % files), defs.remote_env),
            ignore_error=ignore_error
            )
    def palm_chdir( path ):
        return palm.system.chdir( EXP(path), None )

    ###########################################################################
    # REMEMBER CURRENT DIRECTORY
    #
    defs.path_build_binaries_enter = os.getcwd()

    try:
        # CHECK CONFIGURATION IDENTIFIER
        #
        ASSERT(
            defs.configuration_identifier,
            '\n  +++ missing configuration identifier, call with "-c <ID>"'
            )

        # CHECK MAKE COMMAND
        #
        ASSERT(
            defs.build_make_command,
            '\n  +++ no make command found in configuration file "'
            )

        defs.build_cpp_option = ENV_EVAL('build_cpp_option')
        if not defs.build_cpp_option:
            LOG.warn('')
            LOG.warn('  +++ WARNING: no cpp options found in configuration file')

        # CHECK COMPILERNAMES
        #
        ASSERT(
            defs.build_fortranParallel_command,
            '\n  +++ no compiler name for parallel compilation found in configuration file'
            )
        ASSERT(
            defs.build_fortranSerial_command,
            '\n  +++ no compiler name for serial compilation found in configuration file'
            )

        # CHECK COMPILER-OPTIONS
        #
        defs.build_fortranParallel_option = ENV_EVAL('build_fortranParallel_option')
        if not defs.build_fortranParallel_option:
            LOG.warn('')
            LOG.warn('  +++ WARNING: no compiler options found in configuration file')

        # CHECK LINKER-OPTIONS
        #
        defs.build_link_option = ENV_EVAL('build_link_option')
        if not defs.build_link_option:
            LOG.warn('')
            LOG.warn('  +++ WARNING: no linker-options found in configuration file')

        if not defs.run_identifier:

            # CHECK, IF THE BASE DIRECTORY PATH HAS BEEN GIVEN
            #
            ASSERT(
                defs.path_base,
                '\n  +++ no base directory found in configuration file'
                )

            defs.path_base = ENV_EVAL('path_base')
            ASSERT(
                path.isdir( defs.path_base ),
                '\n  +++ base directory "${path_base}" '\
                '\n      does not exist'
                )

            # CHECK, IF THE DEPOSITORY DIRECTORY EXISTS
            #
            defs.make_depository = EXP('MAKE_DEPOSITORY_${configuration_identifier}')
            defs.path_depository = path.join(
                defs.path_base,
                defs.make_depository
                )

            ASSERT(
                path.isdir( defs.path_depository ),
                '\n  +++ depository directory "${path_depository}" '\
                '\n      does not exist'
                )

            palm_chdir('${path_depository}')

        else:
            # CHECK, IF THE TEMPORARY DIRECTORY PATH HAS BEEN GIVEN
            #
            ASSERT(
                defs.fast_io_catalog,
                '\n  +++ no temporary directory found in configuration file'
                )

            defs.fast_io_catalog = ENV_EVAL('fast_io_catalog')
            ASSERT(
                path.isdir( defs.fast_io_catalog ),
                '\n  +++ temporary directory "${fast_io_catalog}" '\
                '\n      does not exist'
                )

            # CHECK, IF THE SOURCE FOR RUN DIRECTORY EXISTS
            #
            defs.sources_for_run_catalog = EXP('SOURCES_FOR_RUN_${configuration_identifier}_${run_identifier}')
            defs.path_run_catalog = path.join(
                defs.fast_io_catalog,
                defs.sources_for_run_catalog
                )

            ASSERT(
                path.isdir( defs.path_run_catalog ),
                '\n  +++ source for run catalog "${path_source_catalog}" '\
                '\n      does not exist'
                )

            palm_chdir('${path_run_catalog}')


        # CHECK MAKEFILE
        #
        defs.makefile = path.join( os.getcwd(), 'Makefile' )
        ASSERT(
            path.isfile( defs.makefile ),
            '\n  +++ makefile: '       \
            '\n           ${makefile}' \
            '\n      does not exist'
            )

        # SET THE ENVIRONMENT (EXECUTE INIT AND MODULE COMMANDS)
        #
        if defs.login_init_cmd:
            if not defs.silent:
                LOG_info('')
            palm_shell('${login_init_cmd}')

        if defs.module_commands:
            try:
                if not defs.silent:
                    LOG_info('')
                palm_shell('${module_commands}')
            except:
                LOG_error('')
                LOG_error('  +++ Module command(s) failed.')
                LOG_error('      Check the above output of the command(s).')
                raise

        # CREATE EXECUTABLE FROM THE NEW/MODIFIED SOURCE FILES, IF THERE ARE ANY
        #
        if not defs.program_name:
            defs.program_name = 'palm'

        message = 'compiling PALM sources on local host'
        if defs.remote_ip:
            message = 'compiling PALM sources on remote host'

        try:
            if not defs.silent:
                LOG_info('')
                LOG_info('  *** ' + message )

            # FIXME: the following configuration parameter are not
            #        supported by Makefile yet:
            #        - build_cpp_command
            #        - build_fortranSerial_option
            #        - build_link_command
            #
            palm_shell(
                '${build_make_command} '
                    '${build_make_option} '
                    'PROG=${program_name} '
                    'F90=${build_fortranParallel_command} '
                    'F90_SER=${build_fortranSerial_command} '
                    'COPT="${build_cpp_option}" '
                    'F90FLAGS="${build_fortranParallel_option}" '
                    'LDFLAGS="${build_link_option}" '
                '| '
                'tee ${configuration_identifier}_last_make_protocol'
                '; '
                'exit $${PIPESTATUS[0]}'
                )

        except subprocess.CalledProcessError:
            LOG.error('')
            LOG_error(
                '\n  +++ error(s) occurred during '+ message +
                '\n  for host configuration "${configuration_identifier}"'
                )

            if defs.show_summary:
                while True:
                    answer = LOG.input('>>> [c]ontinue / [l]ist errors / [a]bort? ').lower()
                    if answer == 'c':
                        break
                    if answer == 'a':
                        raise
                    if answer == 'k':
                        raise
                    if answer == 'l':
                        filename = EXP('${configuration_identifier}_last_make_protocol')
                        with open(filename) as protocol:
                            for line in protocol.readlines():
                                LOG.info( line )
            else:
                raise

        return 0

    finally:
        palm_chdir('${path_build_binaries_enter}')


def build_cli_copy( defs ):

    ###########################################################################
    # useful helper
    #
    def ASSERT( condition, message ):
        assert condition, defs.expand( message )
    def EXP( string, context=None ):
        return defs.expand(string, context=context)
    def ENV( string, context=os.environ ):
        return defs.expand(string, context=context)
    def LOG_info( message ):
        LOG.info( EXP(message) )
    def palm_shell( command, capture_output=False, ignore_error=False ):
        return palm.system.shell( EXP(command), None, capture_output, ignore_error )
    def palm_ssh( command, capture_output=False, ignore_error=False ):
        return palm.system.shell(
            EXP('ssh -q ${remote_key} ${remote_port_ssh} ${remote_user} \'"\'"\' %s \'"\'"\'' % command),
            capture_output=capture_output,
            ignore_error=ignore_error
            )
    def palm_scp( files, ignore_error=False ):
        if not defs.remote_env:
            defs.remote_env = dict( tuple(line.strip().split('=',1)) for line in palm_ssh('env', capture_output=True).splitlines())
        return palm.system.shell(
            ENV( EXP('scp ${remote_key} ${remote_port_scp} %s' % files), defs.remote_env),
            ignore_error=ignore_error
            )
    def palm_chdir( path ):
        return palm.system.chdir( EXP(path), None )

    ###########################################################################
    # REMEMBER CURRENT DIRECTORY
    #
    defs.path_build_cli_copy_enter = os.getcwd()

    try:
        # CHECK PALM CLI DIRECTORY ON REMOTE MACHINE
        #
        ASSERT(
            defs.host_remote_cli_path,
            '\n  +++ no directory for palm command line interface on remote host'
            )

        # CHECK PALM CLI DIRECTORY ON LOCAL MACHINE
        #
        defs.host_local_cli_path = palm.system.cli_dir()

        ASSERT(
            defs.host_local_cli_path,
            '\n  +++ no directory for palm command line interface'
            )
        ASSERT(
            path.isdir( defs.host_local_cli_path ),
            '\n  +++ directory for palm command line interface'\
            '\n         "${host_local_cli_path}"'\
            '\n      does not exist'
            )

        if not defs.silent:
            LOG_info('')
            LOG_info('  *** copying palm command line interface to '  )
            LOG_info('      ${host_remote_ip}:${host_remote_cli_path}')

        # FIXME: using 'rsync' (if possible) could be a better way ...
        #
        palm_shell(
            'cd ${host_local_cli_path}\n'
            'tar '
                '-czf ${host_local_cli_path}/palm-cli.tar.gz '
                '--exclude=\'*/__pycache__\' '
                '--exclude=\'*.tar.gz\' '
                'palm palm-cli'
            )
        palm_scp(
            '${host_local_cli_path}/palm-cli.tar.gz '
            '${remote_user}:~ '
            )
        palm_ssh(
            '\n'

            # CREATE PALM-CLI DIRECTORY, IF IT DOES NOT EXIST
            #
            '  if [[ ! -d "${host_remote_cli_path}" ]]\n'
            '  then\n'
            '    [[ "${verbose}" ]] && printf "\\n  *** ${host_remote_cli_path} will be created\\n"\n'
            '    mkdir -p  ${host_remote_cli_path}\n'
            '  fi\n'

            # UNTAR PALM-CLI ARCHIVE
            #
            '  cd ${host_remote_cli_path}\n'
            '  mv ~/palm-cli.tar.gz .\n'
            '  tar '
                '-xzf palm-cli.tar.gz '
                #'--keep-newer-files '
                '\n'
            )

        return 0

    finally:
        palm_chdir('${path_build_cli_copy_enter}')


def build_summary( defs, columnes=None ):

    if defs.silent or not defs.show_summary:
        return

    defs.show_summary = False # show once each call

    if not columnes:
        columnes = [
            ['config. identifier:',  'configuration_identifier'],
            ['run identifier:',      'run_identifier'          ],
            ['yaml file:',           'yamlfile'                ],
            ['palm file:',           'config_file'             ],
            [],
            ['base dir.:',           'path_base'               ],
            ['depository:',          'make_depository'         ],
            ['depository dir.:',     'path_depository'         ],
            [],
            ['remote username:',     'host_remote_username'    ],
            ['remote address:',      'host_remote_ip'          ],
            ['remote job dir.:',     'host_remote_path_job'    ],
            [],
            ['compiler:',            'compiler_name'           ],
            ['serial compiler:',     'compiler_name_ser'       ],
            ['make options:',        'make_options'            ],
            ['cpp options:',         'cpp_options'             ],
            ['compiler options:',    'compiler_options'        ],
            ['linker options:',      'linker_options'          ],
            ['login init command:',  'login_init_cmd'          ],
            ['module command(s):',   'module_commands'         ]
            ]

    ###########################################################################
    # useful helper
    #
    def EXP( string ):
        return defs.expand( string )
    def update_hostinfo():
        try:
            import socket
            defs.hostname = socket.gethostname()
            defs.local_ip = socket.gethostbyname(defs.hostname)
        except:
            pass
    def LOG_info( message, label, value, length ):

        def columnes(string):
            col_0 = label
            for line in string.splitlines():
                for cut in cut_string( line, length ):
                    yield col_0, cut
                    col_0 = ''

        # NOTE: 'value' might be None, an object or
        #       a (multiline) string
        #
        if value is None:
            value = ''
        elif not isinstance( value, str):
            value = str(value)

        if value:
            for col_0, col_1 in columnes(value):
                LOG.info( message, col_0, col_1 )

        else:
            LOG.info( message, label, '' )


    # get hostname and ip address ...
    #
    update_hostinfo()

    # ... show summery ...
    #
    calltime = datetime.now( timezone.utc ).strftime('%a %b %d %H:%M:%S UTC %Y')

    LOG.info( "" )
    LOG.info( "#------------------------------------------------------------------------#"  )
    LOG.info( "| %-20s%-50s |", 'palm-cli',          calltime                               )
    LOG.info( "| %-20s%-50s |", 'Version:',          EXP('${palm_version_string}')          )
    LOG.info( "| %-70s |",      ""                                                          )
    LOG.info( "| %-20s%-50s |", "called on:",        EXP("${hostname} (IP:${local_ip})")    )
    LOG.info( "| %-70s |",      ""                                                          )

    for columne in columnes:
        if len(columne) == 2:

            (label, name) = columne

            value = defs.get(name)
            if value is None or value == '':
                value = 'N O T  S E T'
            LOG_info( "| %-20s%-50s |", label, value, 50 )

        else:
            LOG.info( "| %-70s |", "" )

    LOG.info( "#------------------------------------------------------------------------#"  )


def cut_string( string, length ):
    result = []
    if string and length and length > 0:
        beg = 0
        end = length
        while True:
            cut = string[beg:end]
            if cut:
                result.append( cut )
                beg  = end
                end += length

            else:
                break

    return result


def parse_option( prog=None, argv=None ):

    # NOTE: this will be much simpler, when the bash case
    #       is not supported anymore

    if argv is None:
        argv = sys.argv[1:]

    parser    = build_argparse( prog )
    opts, ign = parser.parse_known_args( argv )

    opts = dict([ opt for opt in vars(opts).items() ])
    if 'target' in opts:
        opts['binaries_only'] = True if opts['target'] == 'binaries' else False
        del opts['target'] # delete from options
    if 'traceback' in opts:
        palm.traceback = True   # show traceback on error
        del opts['traceback']   # delete from options
    if 'show_summary' not in opts:
        opts['show_summary'] = False if 'silent' in opts else True

    # NOTE: using a second (help) parser to seperate palm
    #       arguments and cli arguments
    #
    parser = argparse.ArgumentParser( add_help=False )
    for option in ['--bash','--log-append', '--no-summary', '--traceback']:
        parser.add_argument( option, action='store_true', default=argparse.SUPPRESS )

    cli, palm_argv = parser.parse_known_args( argv )

    cli_argv = []
    if 'bash' in cli:
        cli_argv.append('--bash')
    if 'log_append' in cli:
        cli_argv.append('--log-append')
    if 'no_summary' in cli:
        cli_argv.append('--no-summary')
    if 'traceback' in cli or palm.traceback:
        cli_argv.append('--traceback')

    return opts, palm_argv, cli_argv


def build_argparse( prog=None ):

    result = argparse.ArgumentParser(
        prog=prog,
        description='''command for compiling the PALM code and its utility programs''',
        add_help=True
        )

    target_choise=['binaries']
    result.add_argument(
        dest='target',
        metavar='<TARGET>',
        choices=target_choise,
        action='store',
        nargs='?',
        default=None, # avoid SUPPRESS in choise
        help='build sub target: '+ ' | '.join(target_choise) + ' '
        )

    #################################################################
    # NOTE: These argmunets are also used in test.py
    #
    result.add_argument(
        '-c',
        metavar='<ID>',
        dest='configuration_identifier',
        type=palm.config.check_arg('-c', 'configuration_identifier', str),
        action='store',
        default='default',
        help='Specifies the so-called configuration identifier. It tells palmrun which configuration file should be used. -c default means to use the configuration file .palm.config.default.'
        )
    result.add_argument(
        '-r',
        metavar='<NAME>',
        dest='run_identifier',
        type=palm.config.check_arg('-r', 'run_identifier', str),
        action='store',
        default=argparse.SUPPRESS,
        help='The name of the run given by -r tells palmrun to use the NAMELIST file <run_identifier>_p3d from JOBS/<run_identifier>/INPUT. It also determines folders and names of output files generated by PALM using informations from the default file configuration file ..../trunk/SCRIPTS/.palm.iofiles. Chapter PALM iofiles explains the format of this file and how you can modify or extend it.'
        )
    result.add_argument(
        '-v',
        dest='silent',
        action='store_true',
        default=argparse.SUPPRESS,
        help='Suppresses parts of palmrun\'s terminal output and prevents palmrun queries'
        )
    result.add_argument(
        '-V',
        dest='use_existing_sources_folder',
        action='store_true',
        default=argparse.SUPPRESS,
        help='Use existing SOURCES_FOR_RUN_... folder. Prevents palmrun from creating a new SOURCES_FOR_RUN_... folder. Use this option if you do not want the user interface files to be compiled again.'
        )
    #
    #################################################################

    result.add_argument(
        '--bash',
        dest='use_bash_script',
        action='store_true',
        default=False,
        help='build PALM configuration from YAML configuration and call original bash script \'palmbuild\' or \'palmrun\''
        )
    result.add_argument(
        '--log-append',
        dest='log_append',
        action='store_true',
        default=argparse.SUPPRESS,
        help='Append logging output to existing file otherwise overwrite existing file '
        )
    result.add_argument(
        '--no-summary',
        dest='show_summary',
        action='store_false',
        default=argparse.SUPPRESS,
        help='Do not show parameter summary . If not set the behavior depends on parameter -v'
        )
    result.add_argument(
        '--traceback',
        dest='traceback',
        action='store_true',
        default=argparse.SUPPRESS,
        help='Show python\'s traceback on error '
        )

    return result


if __name__ == "__main__":
    sys.exit( main() )
