#!/usr/bin/env python3
'''
 Py-ChemShell Installer

 For a guide to the setup options please see the INSTALL file or 
 consult the installation section in the ChemShell manual.
'''

#  Copyright (C) 2019 The authors of Py-ChemShell
#
#  This file is part of Py-ChemShell.
#
#  Py-ChemShell is free software: you can redistribute it and/or modify
#  it under the terms of the GNU Lesser General Public License as
#  published by the Free Software Foundation, either version 3 of the
#  License, or (at your option) any later version.
#
#  Py-ChemShell 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 Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public
#  License along with Py-ChemShell.  If not, see
#  <http://www.gnu.org/licenses/>.

__author__  = ('You Lu <you.lu@stfc.ac.uk>', 'Tom Keal <thomas.keal@stfc.ac.uk>')
__version__ = '25.0.6'

from argparse import ArgumentParser, HelpFormatter, Namespace
from os       import getcwd, environ


CWD = getcwd()

def parseArguments():
    ''''''

    from multiprocessing import cpu_count

    parser = ArgumentParser(prog='setup',
                            description=__doc__,
                            formatter_class=lambda prog: HelpFormatter(prog, max_help_position=21, indent_increment=1, width=120),
                            add_help=False)

    separator = ' '+'-'*70+'\n\n'

    basic      = parser.add_argument_group(title=f'{separator} Basic options',
                                           description='')
    flags      = parser.add_argument_group(title=f'{separator} Compile/Link options',
                                           description='Use these options to choose compilers and their compile/link flags and specify paths to libraries')
    qm_modules = parser.add_argument_group(title=f'{separator} QM codes',
                                           description='Compile/Install optional support for external QM codes')
    mm_modules = parser.add_argument_group(title=f'{separator} MM codes',
                                           description='Compile/Install optional support for external MM codes')
    drivers    = parser.add_argument_group(title=f'{separator} Drivers',
                                           description='Compile/Install optional drivers')
    addons     = parser.add_argument_group(title=f'{separator} Add-ons',
                                           description='Compile/Install additional add-ons')
    platforms  = parser.add_argument_group(title=f'{separator} Platform options',
                                           description='Use presets for supported HPC platforms')

# information

    basic.add_argument('-h', '--help',
                       action='help',
                       help='Show this help message and exit')

    basic.add_argument('-v', '--version',
                       action='version',
                       default=__version__,
                       version='%(prog)s v{}'.format(__version__),
                       help='Show program\'s version number and exit')

    basic.add_argument('--debug',
                       action='store_true',
                       default=False,
                       help='Compile in a debug mode by using debug flags and print verbose screen output')

# build flags

    flags.add_argument('-fc', '--fc', '--fortran-compiler',
                        metavar='',
                        dest='fc',
                        action='store',
                        default='ifort',
                        help='Specify Fortran compiler [ default: %(default)s ]')

    flags.add_argument('-cc', '--cc', '--c-compiler',
                        metavar='',
                        dest='cc',
                        action='store',
                        default='icx',
                        help='Specify C compiler [ default: %(default)s ]')

    # YL 26/09/2020: -cpc isn't a mandatory flag as -fc and -cc, so there shouldn't be a default value
    flags.add_argument('-cpc', '--cpc', '--cpp-compiler',
                        metavar='',
                        dest='cpc',
                        action='store',
                        default='',
                        help='Specify C++ compiler [ default: %(default)s ]')

    flags.add_argument('-fflags', '--fflags',
                        metavar='',
                        action='store',
                        default=[],
                        nargs='*',
                        help='Fortran compiler flags [ default: %(default)s ]')

    flags.add_argument('-cflags', '--cflags',
                        metavar='',
                        action='store',
                        default=[],
                        nargs='*',
                        help='C compiler flags [ default: %(default)s ]')

    flags.add_argument('-ldflags', '--ldflags',
                        metavar='',
                        action='store',
                        default=[],
                        nargs='*',
                        help='Extra link flags to add [ default: %(default)s ]')

    flags.add_argument('--ld_path',
                        metavar='',
                        action='store',
                        default=[],
                        nargs='*',
                        help='Specify ld library path [ default: %(default)s ]')

    flags.add_argument('--math_header_macros',
                        metavar='',
                        action='store',
                        # default is for Ubuntu
                        default='-U__linux__ -U__USE_MISC -U__NetBSD__ -D__FreeBSD__ -U__PURE_INTEL_C99_HEADERS__',
                        help='Resolve the incompatibility between Intel math.h and the system math.h [ default: %(default)s ]')

    # we set a range up to the number of cpu cores on the user's machine
    flags.add_argument('-j', '--jobs',
                        metavar='',
                        action='store',
                        type=int,
                        default=4,
                        choices=range(1,cpu_count()+1),
                        help='Specify the number of make jobs to run simultaneously for building external programs (`make -j` command, no argument for no limit) [ default: %(default)d ]    ')

    flags.add_argument('-O', '-opt', '--opt',
                        metavar='',
                        action='store',
                        default=2,
                        choices=range(4),
                        help='Specify compiler optimisation level [ default: %(default)d ]')

    flags.add_argument('-i8', '--i8',
                        action='store_true',
                        default=False,
                        help='Build 64-bit binary [ default: %(default)r ]')

    flags.add_argument('-f128', '--f128',
                        action='store_true',
                        default=False,
                        help='Impose -D_Float128=__float128 to fix the conflict between Intel 17 and gcc 7 [ default: %(default)r ]')

    flags.add_argument('--cmake',
                        metavar='',
                        dest='cmake',
                        action='store',
                        default='cmake',
                        help='Specify the CMake command [ default: %(default)s ]')

    flags.add_argument('--arch',
                        metavar='',
                        action='store',
                        default='',
                        choices=['cray','gnu','intel','cpe-gnu'],
                        help='Override the ChemShell build architecture that is otherwise automatically detected according to the compilers [ default: %(default)s ]')

    flags.add_argument('--python_root_dir',
                        metavar='',
                        action='store',
                        default='',
                        help='Specify path to the root directory of a Python 3 installation. [ default: %(default)s ]')

    flags.add_argument('--prepend-pythonpath',
                        metavar='',
                        action='store',
                        default='',
                        help='Path(s) to additional Python packages to prepend to sys.path (NB: not the environment variable PYTHONPATH); seperate with : when there are more than one; package paths prepended will override the default system ones if there are duplicates [ default: %(default)s ]')

    flags.add_argument('-mkl', '--mkl',
                        metavar='',
                        action='store',
                        default="",
                        help='Enable Intel MKL and specify MKL library path [ default: %(default)s ]')

    # compatible with default --i8=false
    flags.add_argument('--mkl_flags',
                        metavar='',
                        action='store',
                        default='',
                        help='Intel MKL arguments for linking [ default: %(default)s ]')

    # YL TODO 05/03/2021: will rename --mkl_flags as --mkl_link_flags pending the MKL FLAGS Issue to be addressed
    flags.add_argument('--mkl_compile_flags',
                        metavar='',
                        action='store',
                        default='',
                        help='Intel MKL arguments for compiling [ default: %(default)s ]')

    flags.add_argument('--blas',
                        metavar='',
                        action='store',
                        default='default',
                        help='Specify BLAS path [ default: %(default)s ].' +
                             ' If "default", find_package will be used; ' +
                             'otherwise provide a path directly to the ' +
                             'object (i.e. usually libblas.so/libblas.a)' +
                             'or a complete set of compilation flags.')

    flags.add_argument('--lapack',
                        metavar='',
                        action='store',
                        default='default',
                        help='Specify LAPACK path [ default: %(default)s ]' +
                             ' If "default", find_package will be used; ' +
                             'otherwise provide a path directly to the ' +
                             'object (i.e. usually liblapack.so/libblapack.a)' +
                             'or a complete set of compilation flags.')

    flags.add_argument('--fftw',
                        metavar='',
                        action='store',
                        default='',
                        help='Specify the path to FFTW library directory [ default: %(default)s ]')

    flags.add_argument('--fftw_link_flags',
                        metavar='',
                        action='store',
                        default='',
                        help='Specify the FFTW linker flags[ default: %(default)s ]')

    flags.add_argument('--fftw_include_dir',
                        metavar='',
                        action='store',
                        default='',
                        help='Specify the full path of directory containing FFTW header files [ default: %(default)s ]')

    flags.add_argument('--scalapack',
                        nargs='?',
                        action='store',
                        metavar='',
                        default='',
                        help='Path to ScaLAPACK library directory: ScaLAPACK switched on using default settings if no argument provided\n[ default: %(default)s ]')

    flags.add_argument('--scalapack_flags',
                        metavar='',
                        action='store',
                        default='',
                        help='ScaLAPACK arguments for linking [ default: %(default)s ]')

    flags.add_argument('-mpi', '--mpi',
                       nargs='?',
                       action='store',
                       default=False,
                       help='[Specify MPI implementation] to enable MPI parallel build, for example: --mpi or --mpi=openmpi or --mpi=intel [default: %(default)r]')

    flags.add_argument('--mpi_include_path',
                        metavar='',
                        action='store',
                        default='',
                        help='Include path(s) for MPI header, for example: --mpi_include_path=/usr/lib/x86_64-linux-gnu/openmpi/include [ default: %(default)s ]')

    flags.add_argument('--mpi_lib_path',
                        metavar='',
                        action='store',
                        default='',
                        help='Specify MPI lib path (use semicolon ";" if there are more than one), for example: --mpi_lib_path=/usr/lib/x86_64-linux-gnu/openmpi/lib; this will override the MPI option chosen by Fortran compiler [ default: %(default)s ]')

    flags.add_argument('--mpi_libraries',
                        metavar='',
                        action='store',
                        default='',
                        help='MPI shared libraries separated by semicolon (;) [ default: %(default)s ]')

    flags.add_argument('-mpiexec', '--mpiexec',
                        metavar='',
                        action='store',
                        default='',
                        help='Executable program of MPI: could be mpirun.openmpi or mpiexec.openmpi for OpenMPI if the latter coexists with Intel MPI in your system [ default: %(default)s ]')

    flags.add_argument('--tcl_include_path',
                        metavar='',
                        action='store',
                        default='',
                        help='Include path(s) for Tcl header files [ default: %(default)s ]')

    # YL 20/03/2024: added a switch because sometimes pip update/upgrade is unnecessary
    flags.add_argument('--no-pip',
                       action='store_true',
                       default=False,
                       help='[Not to upgrade pip version nor install Python dependencies using pip [default: %(default)r]')

    flags.add_argument('-clean', '--clean',
                        metavar='',
                        nargs='*',
                        default='all',
                        help='Clear the existing compilation')

    # modules to recompile
    flags.add_argument('-rb', '--rebuild', '--recompile',
                       action='store',
                       dest='rebuild',
                       nargs='*',
                       default=[],
                       help='Specify module(s) of the external codes/drivers/addons to recompile [ default: %(default)r ]')

    flags.add_argument('--rebuild-without-download', '--recompile-without-download',
                       action='store_true',
                       dest='rebuild_without_download',
                       default=False,
                       help='Do not redownload source code before rebuilding the external code [ default: %(default)r ]')

    flags.add_argument('--gpu','-gpu',
                        nargs='?',
                        action='store',
                        dest='gpu',
                        default='',
                        help='Specify the gpu api to build external codes with gpu support, eg. cuda,hip,openacc [ default: %(default)r ]',)

    flags.add_argument('-gpucc', '--gpucc', '--gpu-c-compiler',
                        metavar='',
                        dest='gpucc',
                        action='store',
                        default='',
                        help='Specify GPU compiler,eg; nvcc/nvfortran, fullpath or name [ default: %(default)s ]')

    flags.add_argument('--cuda-arch','--gpu-arch','-cuda-arch','-gpu-arch',
                        action='store',
                        dest='gpuarch',
                        default='',
                        help='Specify the cuda-arch as a string or number to build external codes with gpu support, e.g.,85 or A100 [ default: %(default)r ]',)

    flags.add_argument('--cuda-version','--cuda-ver', '-cuda-version', '-cuda-ver',
                        action='store',
                        dest='cudaversion',
                        default='',
                        help='Specify the cuda-version to use when more than one version is installed [ default: %(default)r ]',)
# QM codes

   # Castep
    qm_modules.add_argument('--castep',
                            metavar='',
                            action='store',
                            default='',
                            help='Enable direct linking to a pre-compiled CASTEP library and specify location of CASTEP library [ default: %(default)s ]')

    # CP2K
    qm_modules.add_argument('--cp2k',
                            metavar='',
                            nargs='?',
#                            action='store_const',
                            default=False,
                            const='https://github.com/cp2k/cp2k.git',
                            help='Enable CP2K build: download CP2K from the GitHub repository if no parameter is given; otherwise specify CP2k location--the directory where dft, diesel, etc are contained [ default: %(default)s ]')

    qm_modules.add_argument('--cp2k-reinstall',
                            action='store_true',
                            default=False,
                            help='Force to reinstall CP2K libraries [ default: %(default)r ]')

    # FHI-aims
    qm_modules.add_argument('--fhiaims',
                            metavar='',
                            action='store',
                            default='',
                            help='Enable direct linking to a pre-compiled FHI-aims library and specify location of FHI-aims library [ default: %(default)s ]')

    # GAMESS-UK
    qm_modules.add_argument('--gamess-uk',
                            metavar='',
                            action='store',
                            default='',
                            help='Enable GAMESS-UK and specify GAMESS-UK location--the directory where dft, diesel, etc are contained [ default: %(default)s ]')

    # LSDalton
    qm_modules.add_argument('--lsdalton',
                            metavar='',
                            nargs='?',
                            action='store',
                            default=False,
                            const='https://gitlab.com/dalton/lsdalton.git',
                            help='Enable LSDALTON and optionally specify LSDALTON location--either the directory where source is contained or a tarball; if no parameter given the official GitLab repository is used [ default: %(default)s ]')

    # NWChem
    # YL 05/12/2021: three scenarios are allowed for
    #                  * --nwchem: download and from the NWChem repository and build automatically
    #                  * --nwchem xxx/yyy/zzz: build pre-downloaded NWChem source files in directory xxx/yyy/zzz
    #                  * NWChem will not be linked in if no such argument given
    qm_modules.add_argument('--nwchem',
                          metavar='',
                          nargs='?',
                          default=False,
#                          const='https://github.com/nwchemgit/nwchem/archive/refs/heads/hotfix/release-7-2-0.zip',
                          # TD 03/09/2024: Issues on Archer2 with 7.2.3
                          # TD 02/09/2025: There are a number of different options for NWChem source files
                          #                But I think we should use the "srconly" ones in order to skip the
                          #                time consuming 64_to_32 bit conversion
                          const='https://github.com/nwchemgit/nwchem/releases/download/v7.2.2-release/nwchem-7.2.2-release.revision-74936fb9-srconly.2023-11-03.tar.bz2',
                          help='Enable direct linking to pre-compiled NWChem libraries by specifying NWChem root location; if no location is specified NWChem will be downloaded from the official repository [ default: %(default)s ]')

# MM codes

    # DL_POLY
    mm_modules.add_argument('--dl_poly',
                            metavar='',
                            nargs='?',
                            action='store',
                            default=False,
                            const='https://gitlab.com/ccp5/dl-poly.git',
                            help='Enable DL_POLY 5 and optionally specify DL_POLY 5 location--either the directory where source is contained or a tarball; if no parameter given the official GitLab repository is used [ default: %(default)s ]')

    # GULP
    mm_modules.add_argument('--gulp',
                            metavar='',
                            action='store',
                            default='',
                            help='Enable GULP and specify GULP location--either the directory where Src is contained or a tarball [ default: %(default)s ]')

    # NAMD
    mm_modules.add_argument('--namd',
                            metavar='',
                            action='store',
                            default='',
                            help='Enable NAMD and specify NAMD location: either the directory where Src is contained or a tarball [ default: %(const)s ]')

# Drivers

    # DL_FIELD
    drivers.add_argument('--dl_field',
                         metavar='',
                         action='store',
                         default='',
                         help='Specify the DL_FIELD location [ default: %(default)s ]')

    # DL_MONTE
    drivers.add_argument('--dl_monte',
                         metavar='',
                         nargs='?',
#                         action='store_const',
                         default=False,
#                         const='https://gitlab.com/dl_monte/DL_MONTE-2',
                         const='https://gitlab.com/dl_monte/dl_monte-releases.git',
                         help='Enable DL_MONTE build: download DL_MONTE from the GitLab repository if no parameter is given; otherwise specify DL_MONTE location--the directory where dft, diesel, etc are contained [ default: %(default)s ]')

    drivers.add_argument('--dl_monte_htk',
                            metavar='',
                            action='store',
                            default='https://gitlab.com/dl_monte/dlmontepython.git',
                            help='DL_MONTE HTK GitLab repository; or the directory where subdirectories "doc", "examples", and "htk" are contained [ default: %(default)s ]')

    # Global Arrays (GA)
    drivers.add_argument('--ga',
                         metavar='',
                         action='store',
                         default='',
                         help='Enable Global Arrays (GA) and specify a GA location [ default: GA in the NWChem distribution ]')

    drivers.add_argument('--ga-no-configure',
                         action='store_true',
                         default=False,
                         help='Skip GA configure [ default: %(default)r ]')
    
    drivers.add_argument('--ga-armci-network',
                         metavar='',
                         action='store',
                         default='MPI-PR',
                         help='Specify NWChem ARMCI_NETWORK flag (see https://nwchemgit.github.io/ARMCI.html for options) [ default: MPI-PR ]')

# Add-ons

    # Aten GUI
    addons.add_argument('--aten',
                        metavar='',
                        action='store_const',
                        default=False,
                        const='https://github.com/trisyoungs/aten.git',
                        help='Download and compile the Aten GUI (see: www.projectaten.com) [ default: %(default)s ]')

    # CHARMM forcefield
    # --charmm-ff-version must be defined before --charmm-ff!
    addons.add_argument('--charmm-ff-version',
                        metavar='',
                        action='store',
                        default='c36_feb26',
                        help='Version of CHARMM forcefield files to install, see --charmm-ff [ default: %(default)s ]')

    addons.add_argument('--charmm-ff',
                        metavar='',
                        action='store_const',
                        default=False,
                        const='http://mackerell.umaryland.edu/download.php?filename=CHARMM_ff_params_files/toppar_{}.tgz'.format(addons.get_default('charmm_ff_version')),
                        help='Download CHARMM forcefield files from http://mackerell.umaryland.edu/charmm_ff.shtml and install to chemsh/data/charmm. See INSTALL if your machine cannot access the internet [ default: %(default)s ]')

    # BSE
    addons.add_argument('--bse', '--molssi-bse',
                        metavar='',
                        dest='bse',
                        action='store_const',
                        default=False,
                        const='https://github.com/MolSSI-BSE/basis_set_exchange',
                        help='Download and install BSE (Basis Set Exchange). See INSTALL if your machine cannot access the internet [ default: %(default)s ]')

    # PDB2PQR
    addons.add_argument('--pdb2pqr',
                        metavar='',
                        action='store_const',
                        default=False,
                        # YL 18/03/2021: upgraded to PDB2PQR 3.1 which is Python3
                        const='https://github.com/Electrostatics/pdb2pqr.git',
                        help='Download and install PDB2PQR. See INSTALL if your machine cannot access the internet [ default: %(default)s ]')

    # PROPKA
    addons.add_argument('--propka',
                        metavar='',
                        action='store_const',
                        default=False,
                        const='https://github.com/jensengroup/propka.git',
                        help='Download and install PROPKA (You do not have to use this argument if you use --pdb2pqr already because PDB2PQR contains PROPKA) [ default: %(default)s ]')

# platform options

    platforms.add_argument('--platform',
                            metavar='',
                            action='store',
                            default='',
                            help='Choose the platform type [ default: %(default)s ]')

    platforms.add_argument('--force-serial',
                            nargs='?',
                            action='store',
                            default=False,
                            help='Enforce a serial build without MPI (only valid for HPC platforms defaulted to --mpi) [default: %(default)r]')

    platforms.add_argument('--load-modules',
                            metavar='',
                            action='store',
                            default=[],
                            nargs='+',
                            help='Modules to load using the Environment Modules system [ default: %(default)s ]')

    platforms.add_argument('--unload-modules',
                            metavar='',
                            action='store',
                            default=[],
                            nargs='+',
                            help='Modules to unload using the Environment Modules system [ default: %(default)s ]')

# end of arguments

    args = parser.parse_args()

    # the default values are still valuable in future check
    args.__get_default = parser.get_default
    args.__parser = parser

    return args


def getInitCmd(args):
    ''''''


def getHostname():
    '''Return the hostname'''

    from socket import gethostname

    return gethostname().split('.')[0]


def getSetupArgs():
    ''''''

    import sys

    strbuff = ' '.join(sys.argv)

    return strbuff


def isURL(location):
    '''Is a URL or not'''

    if type(location) is not str:
        return False

    if location.lower().startswith('http'):
        return True
    else:
        return False


def getPathOrURL(location):
    '''Return the original str if it is a URL or absolute path otherwise'''

    from os import path

    # blanc str
    if not location:
        return location
    # URL
    if isURL(location):
        return location
    else:
        # ~/abc/xyz
        if location.startswith('~'):
            return path.expanduser(location)
        # absolute path
        if path.isabs(location):
            # YL 10/11/2021: removed strict=True which is not available until Python 3.10
            return path.realpath(location)
        # relative path: join with CWD because we did a chdir('chemsh')
        else:
            return path.abspath(path.join(CWD, location))


def getCMakeCmd(args, stage="configure"):
    ''''''

    from textwrap import wrap, indent

    bools = { True :'ON',
              False:'OFF',
              1    :'ON',
              0    :'OFF',
            }

    command = args.cmake

# YL 14/09/2025: moved to chemsh/CMakeLists.txt because not all MPI compiler wrappers are called "xxxmpiyyy", e.g., ftn or CC
#    # TD: 20/09/2024 : Raise a warning if a serial build has been requested with MPI compiliers
#    if (('mpi' in args.fc) or ('mpi' in args.cc) or ('mpi' in args.cpc)) and args.mpi == False:
#        print(f'\n >>> WARNING: A MPI compilier has been given but an MPI build is not requested (--mpi) \n')

    # YL: used to believe that we should not define CMAKE_C_COMPILER or CMAKE_Fortran_COMPILER here for CMakeCache.txt, but only FC and/or CC instead which would be used for CMAKE_C_COMPILER and CMAKE_Fortran_COMPILER in CMakeLists.txt. however this does not (nor -UCMAKE_XXX_COMPILER) solve the severe problem that the command-line variables are completely ignored when CMake enforces a restart by compiler change (this problem also damages bin/${CHEMSH_ARCH}/chemsh.py to be installed). now we always do the configuring in two stages: 1. change the compilers only 2. do a full configuration
#    if args.mpi:
#        command += ' -DMPI_Fortran_COMPILER:FILEPATH='+args.fc
#        command += ' -DMPI_C_COMPILER:FILEPATH='+args.cc
#        command += ' -DMPI_CXX_COMPILER:FILEPATH='+args.cpc
#    else:
#        command += ' -DCMAKE_Fortran_COMPILER:FILEPATH='+args.fc
#        command += ' -DCMAKE_C_COMPILER:FILEPATH='+args.cc
#        command += ' -DCMAKE_CXX_COMPILER:FILEPATH='+args.cpc
    command += ' -DCMAKE_Fortran_COMPILER:FILEPATH='+args.fc
    command += ' -DCMAKE_C_COMPILER:FILEPATH='+args.cc
    command += ' -DCHEMSH_ARCH:STRING='+args.arch.lower()
    if args.cpc.strip():
        command += ' -DCMAKE_CXX_COMPILER:FILEPATH='+args.cpc

    if stage == "initialise":

        command += ' --no-warn-unused-cli'
        command += ' .'

        print("\n >>> setup command:\n    ", command, "\n\n")

        return command

    command += ' -DHOSTNAME:STRING='+getHostname()

    command += ' -DCHEMSH_MAKE_NJOBS:STRING='+str(args.jobs)

    command += ' -DCHEMSH_64_BIT:BOOL='+bools[args.i8]

    command += ' -DCHEMSH_Fortran_FLAGS:STRING="'+' '.join(args.fflags) + '"'

    # Intel compiler 17 is not compatible with GNU 7 (it has been fixed after 18.0.1)
    if args.f128:
#        args.cflags.append('-D_Float128=__float128')
        command += ' -DF128:STRING="-D_Float128=__float128"'

    command += ' -DCHEMSH_C_FLAGS:STRING="'+' '.join(args.cflags) + '"'

    command += ' -DCHEMSH_LD_FLAGS:STRING="'+' '.join(args.ldflags) + '"'

    # this information is only injected by platforms specification so can't guanrantee it exists
    try:
        command += ' -DCHEMSH_EXTRA_FFLAGS:STRING="'+args._extra_fflags+'"'
        command += ' -DCHEMSH_EXTRA_CFLAGS:STRING="'+args._extra_cflags+'"'
        command += ' -DCHEMSH_EXTRA_CTYPEDEFS:STRING="'+args._extra_ctypedefs+'"'
    except:
        pass

    if args.debug:
        command += ' -DDEBUG:BOOL=ON'
        command += ' -L'               # list non-advanced flags
        command += ' --debug-output'
# YL 05/11/2022: this argument doesn't exit in some versions?
#        command += ' --verbose'
        # -O0
        args.opt = 0
    # suppress "CMake Warning: Manually-specified variables were not used by the project: ......"
    else:
        command += ' --no-warn-unused-cli'

    command += ' -DCHEMSH_COMPILE_OPT_LEVEL:STRING='+str(args.opt)

    command += ' -DLD_LIBRARY_PATH="-L'+' -L'.join(args.ld_path)+'"'

    command += ' -DCHEMSH_LD_LIBRARY_PATH:PATH="'+':'.join(args.ld_path)+'"'

    command += ' -DPLATFORM:STRING='+args.platform

    # CMake hint for python install to use
    if args.python_root_dir:
        command += ' -DPython3_ROOT_DIR:PATH='+args.python_root_dir

    # additional PYTHONPATH
    command += ' -DPYTHONPATH='+args.prepend_pythonpath

    # modules to recompile
    rebuildables = [ 'aten', 'castep', 'charm++', 'cp2k', 'dl_monte', 'dl_poly', 'ga', 'gamess-uk', 'gulp', 'lsdalton', 'namd', 'nwchem' ]
    for rebuildable in rebuildables:
        command += ' -D{}_RECOMPILE:BOOL=OFF'.format(rebuildable.upper())
    for mod in args.rebuild:
        if mod.lower() in rebuildables and args.clean == 'all':
            command += ' -D{}_RECOMPILE:BOOL=ON'.format(mod.upper())
    if args.rebuild_without_download:
        command += ' -DRECOMPILE_WITHOUT_DOWNLOAD:BOOL=ON'
    else:
        command += ' -DRECOMPILE_WITHOUT_DOWNLOAD:BOOL=OFF'

    # YL 09/11/2021: all locations should be wrapped around by getPathOrURL() to make sure relative paths work

    # BSE
    if args.bse:
        command += ' -DBSE='+getPathOrURL(args.bse)

    # PDB2PQR and PROPKA
    if args.pdb2pqr:
        command += ' -DPDB2PQR='+getPathOrURL(args.pdb2pqr)
    if args.propka:
        command += ' -DPROPKA='+getPathOrURL(args.propka)
    # YL 18/03/2021: now PPOPKA is a dependency of PDB2PQR
    if args.propka == False and args.pdb2pqr:
        command += ' -DPROPKA='+args.__parser.parse_known_args(['--propka'])[0].propka

    # GA
    command += ' -DGA='+getPathOrURL(args.ga)
    command += ' -DGA_RECONFIGURE='+bools[not args.ga_no_configure]
    command += ' -DGA_ARMCI_NETWORK='+args.ga_armci_network.upper()

    # GPU: can be empty args to denote 'on'
    if args.gpu:
        try:
            command += ' -DGPU='+args.gpu
        except:
            command += ' -DGPU=TRUE'

    if args.gpuarch:
        command += ' -DGPU_ARCH='+args.gpuarch

    if args.gpucc:
        command += ' -DGPU_COMPILER:STRING='+args.gpucc

    if args.cudaversion:
        command += ' -DCUDA_VERSION='+args.cudaversion

    # CASTEP
    if args.castep:
        command += ' -DCASTEP:STRING='+getPathOrURL(args.castep)

    # CP2K
    if args.cp2k:
        command += ' -DCP2K='+getPathOrURL(args.cp2k)

    # DL_FIELD
    command += ' -DDL_FIELD='+getPathOrURL(args.dl_field)

    # DL_POLY
    if args.dl_poly:
        command += ' -DDL_POLY='+getPathOrURL(args.dl_poly)

    # DL_MONTE
    if args.dl_monte:
        command += ' -DDL_MONTE='+getPathOrURL(args.dl_monte)
        command += ' -DDL_MONTE_HTK='+getPathOrURL(args.dl_monte_htk)
  
    # GULP
    command += ' -DGULP:PATH='+getPathOrURL(args.gulp)

    # "-" is not allowed, use "_"
    command += ' -DGAMESS-UK:PATH='+getPathOrURL(args.gamess_uk)

    # NAMD
    if args.namd:
        if args.__CHEMSH_ARCH == 'intel' and not args.mpi:
            message = "A multi-core version build of Charm++-driven NAMD by Intel compilers is requested. It may fail due to a known system issue on Ubuntu. If this happens please try an MPI version with --mpi and MPI compiler wrappers.\n"
            print('\n'+indent('\n'.join(wrap(message, width=70)), ''))
        if not args.cpc:
            print("\n Compiling NAMD requires a C++ compiler. Please specify one using -cpc or --cpc or --cpp-compiler\n")
            exit(112)
        command += ' -DNAMD:PATH='+getPathOrURL(args.namd)

    # CHARMM forcefield
    if args.charmm_ff:
        command += ' -DCHARMM_FF='+str(args.charmm_ff)

    # fhi-AIMS
    command += ' -DFHIAIMS:PATH='+getPathOrURL(args.fhiaims)

    # NWChem
    if args.nwchem:
        command += ' -DNWCHEM:STRING='+getPathOrURL(args.nwchem)
        # more targets for the future
        command += ' -DNWCHEM_TARGET:STRING=LINUX64'
        command += ' -DNWCHEM_SCRIPT:PATH='+str(args.__nwchem_script)
        # we can't directly use ; in command line for it will be interpreted by shell otherwise
        command += ' -DNWCHEM_PATCHES:INTERNAL='+':'.join(args._nwchem_patches)

    # LSDalton
    if args.lsdalton:
        command += ' -DLSDALTON='+getPathOrURL(args.lsdalton)

    # Aten GUI
    if args.aten:
        command += ' -DATEN='+getPathOrURL(args.aten)

    # macros for math.h
    command += ' -DMATH_HEADER_MACROS='+'"'+args.math_header_macros+'"'

    command += ' -DBLAS_DIR:PATH='+args.blas
    command += ' -DLAPACK_DIR:PATH='+args.lapack
    command += ' -DFFTW_DIR:PATH='+args.fftw
    command += ' -DFFTW_INCLUDE_DIR:PATH='+args.fftw_include_dir
    command += ' -DFFTW_LINK_FLAGS:STRING='+'"'+args.fftw_link_flags+'"'

    # ScaLAPACK
    if args.scalapack:
        try:
            command += ' -DScaLAPACK_DIR:PATH='+args.scalapack
        except:
            pass
    if args.scalapack_flags:
        command += ' -DScaLAPACK_FLAGS:STRING='+'"'+args.scalapack_flags+'"'

    # YL TODO 05/03/2021: I sugget renaming MKL to MKLROOT to be in line with Intel MKL
    #                     will do it after the Issue of MKL flags is addressed
    command += ' -DMKL='+getPathOrURL(args.mkl)
    #Raj: 13-11-24 use a flag with meaningful name;keeping the old as well, to prevent breaking
    command += ' -DMKL_LIB='+getPathOrURL(args.mkl)
    command += ' -DMKL_FLAGS:STRING='+'"'+args.mkl_flags+'"'
    command += ' -DMKL_COMPILE_FLAGS:STRING='+'"'+args.mkl_compile_flags+'"'

    # support for MPI parallelism
    if args.mpi:
        command += ' -DMPI:BOOL='+bools[args.mpi]
        command += ' -DMPIEXEC_EXECUTABLE:FILEPATH='+args.mpiexec

    if args.mpi_libraries:
        command += ' -DMPI_LIBRARIES:STRING='+args.mpi_libraries

    if args.mpi_lib_path:
        command += ' -DMPI_LIB_PATH:PATH='+args.mpi_lib_path

    if args.mpi_include_path:
        command += ' -DMPI_INCLUDE_PATH:PATH='+args.mpi_include_path

    if args.tcl_include_path:
        command += ' -DTCL_INCLUDE_PATH:PATH='+args.tcl_include_path

    # YL 20/03/2024: added a switch of pip upgrading/updating
    command += ' -DPIP:BOOL='+bools[not args.no_pip]

    try:
        # pass in the multi-line string as a CMake list
        command += ' -DCHEMSH_BASH_HEADER_BUFF:STRING='+'\"'+';'.join(args.__bash_header)+'\"'
        command += ' -DCHEMSH_BASH_FOOTER_BUFF:STRING='+'\"'+';'.join(args.__bash_footer)+'\"'
#        command += ' -DCHEMSH_BASH_ARGS:STRING='+'"'+args.__bash_args+'"'
    except:
        pass

    # silent if non-debug
    if not args.debug:
        command += ' --no-warn-unused-cli -Wno-dev'

    command += ' .'

    if args.debug:
        print("\n >>> setup command:\n    ", command, "\n\n")

    # Check for mistakes in the generated command str
    validateCommandStr(command)

    return command

# TD 11/09/2024: Helper function to raise a warning if an incorrect path is being given to cmake
def validateCommandStr(command):
    '''Run some sanity checks on the generated CMake command'''

    from os.path import isdir, isfile
    import shutil

    # Get the individual components and discard the ones that are not options
    token_list = command.split()
    token_list.pop(0)  # cmake
    token_list.pop(-1) # .

    # Look at the options one by one
    for token in token_list:
        # CMake CACHE entry
        if token[0:2] == '-D':
            try:
                option_type = ''
                option, val = token.split('=')
                if ':' in option:
                    option, option_type = option.split(':')

                # Now run some simple validation
                # PATH should be blank, valid directory or list of valid directories (separated by ;)
                # (or "default" which means that CMake will try and find it)
                if option_type == 'PATH':
                    # A blank value is allowed
                    if val == '' or val == '""' or val == 'default':
                        continue
                    # But a definite value should point to an accessible directory
                    for path in val.split(';'):
                        path = path.strip('"')
                        if not(isdir(path)):
                            print(f'\n >>> WARNING: A path given to CMake is not an accessible directory ({option} {path}) \n')

                # FILEPATH should be blank, valid filepath or list of valid filepaths (separated by ;)
                elif option_type == 'FILEPATH':
                    # A blank value is allowed
                    if val == '' or val == '""':
                        continue
                    # But a definite value should point to an accessible file
                    for path in val.split(';'):
                        path = path.strip('"')
                        # Test if path to file
                        is_file = isfile(path)
                        # Test if program on path
                        is_program = shutil.which(path) is not None
                        if not(is_file or is_program):
                            print(f'\n >>> WARNING: A file given to CMake is not an accessible file ({option} {path}) \n')

            # I don't think this should happen with a valid CMake CACHE entry
            except ValueError:
                # ... apart from this maths flag
                if token == '-D__FreeBSD__':
                    pass
                else:
                    # TD 03/10/2025: Printing too many false positives for now
                    pass
                    #print(f'\n >>> WARNING: A CMake option ({token}) looks incorrect \n')

    return

# YL 29/06/2022: changed to return the wanted value to be used as a property() function at a deferred stage (in the PLATFORM.setup() function)
def getArch(args):
    '''Determine the compiler architechture'''

    from subprocess import check_output, DEVNULL

    # YL 02/08/2023: Intel's transition to LLVM is disastrous!
    #                mpiifx, which is obliged to combine with -fc=, can't be used alone,
    #                however neither mpiifx -fc=ifort nor -fc=ifx is legal
    if args.fc == 'mpiifx':
        args.fc = 'mpiifx'  # YL 27/06/2024: now mpiifort no longer exists and only mpiifx should be used
        args.__fc = 'ifx'
    else:
        args.__fc = ''
    if args.cc == 'mpiicx':
        args.__cc = 'icx'
    else:
        args.__cc = ''
    if args.cpc == 'mpiicx':
        args.__cpc = 'icx'
    else:
        args.__cpc = ''

    def _getCompiler(cmd):
        '''Get the underlying compiler'''

        if not cmd.strip():
            return ''

        # YL 02/08/2023: '--showme:command' - OpenMPI
        #                '-show'            - Intel (mpiifort/mpiicc)
        #                '--version'        - Cray (ftn) or Intel(mpiicx)
        for arg in [ '--showme:command', '-show', '--version' ]:
            try:
                out = check_output(cmd.split()+[arg], stderr=DEVNULL)
            except:
                out = b''
        # YL 26/09/2020: allow args.cpc to be blank
        try:
            return out.split()[0].decode()
        except:
            return out.decode()

    # underlying compilers
    # YL 02/08/2023: only assign if not defined earlier
    if not args.__fc:
        args.__fc  = _getCompiler(args.fc)
    if not args.__cc:
        args.__cc  = _getCompiler(args.cc)
    if args.cpc and not args.__cpc:
        args.__cpc = _getCompiler(args.cpc)
   
    selectcases = { 'ifort'   :'intel',
                    'ifx'     :'intel',
                    'Intel(R)':'intel',
                    'gfortran':'gnu',
                    'GNU'     :'gnu',
                  }

    return selectcases.get(args.__fc, '')


# YL 29/06/2022: moved out and changed to return the wanted value to be used as a property() function at a deferred stage (in the PLATFORM.setup() function)
def getFCVersion(args):
    '''Get the FORTRAN compiler's version number'''

    from subprocess import check_output, DEVNULL

    out = b''
    for arg in [ args.fc.split(), args.fc.split()+[f'-fc={args.__fc}'] ]:
        try:
            out = check_output(arg+['--version'], stderr=DEVNULL)
        except:
            pass

    version = 'unknown'

    if args.__CHEMSH_ARCH == 'intel':
        try:
            version = str(int(out.decode().split()[3])/10000)
        except:
            pass
            
    elif args.__CHEMSH_ARCH == 'gnu':
        try:
            version = '.'.join(out.decode().split()[4].split('.')[:2])
        except:
            pass

    # TD 18/08/2025: I took this warning off again as it will trigger on the first pass with "ifort" on systems
    #                without ifort
    #if version == 'unknown':
    #    print("\n >>> WARNING: Fortran compiler version was not identified successfully. Build may fail\n")

    return version


def writeSetupLog(args, filename):
    '''Write setup script arguments to setup.log'''

    from time    import time as time_time
    from time    import asctime, localtime
    from getpass import getuser

    with open(filename, 'a') as fp:
        strbuff = "" + 24*"=" + "\n"
        fp.write(strbuff)
        strbuff = "" + asctime(localtime(time_time())) + "\n"
        fp.write(strbuff)
        strbuff = "" + 24*"=" + "\n\n"
        fp.write(strbuff)
        strbuff = "By " + getuser() + "\n\n"
        fp.write(strbuff)
        fp.write(getSetupArgs())
        fp.write('\n\n')
        strbuff = getCMakeCmd(args)
        fp.write(strbuff)
        fp.write('\n\n\n')


def choosePlatform(args):
    '''Set up environment for a chosen platform'''

    global args_copy

    import platform
    from os.path import dirname, join, realpath

    # TD 10/02/2025: Determine platform (user argument or default if on Ubuntu)
    if args.platform:
        args.platform = args.platform.lower()
    else:
        # Check if Ubuntu
        if platform.system() == 'Linux':
            try:
                if platform.freedesktop_os_release()['ID'] == 'ubuntu':
                    args.platform = 'ubuntu'
            # TD 20/03/2025: This happens on DAaaS (platform module too old)
            except AttributeError:
                pass

    args.__CHEMSH_ROOT = realpath(dirname(__file__))

    # TD 10/02/2025: Report the selected platform file
    if args.platform:
        platform_file = join(args.__CHEMSH_ROOT, 'chemsh', 'utils', 'platforms', args.platform+'.py')
        print('Build using platform file:', platform_file)

    args.__import = importModule
    args.__getPathOrURL = getPathOrURL

    # YL 29/06/2022: made these parameters dynamically determined because they won't be used until the calling to platfmodule.setup(args)
    #                because they depend on args.fc which is usually defined in platfmodule.setup()
    Namespace.__CHEMSH_ARCH = property(getArch)
    # YL 01/07/2022: __fc_version duplicated with __FC_VERSION, and now merged
    Namespace.__FC_VERSION  = property(getFCVersion)

    # load platform tools and specifications
    platfmodule = importModule('platforms', join(args.__CHEMSH_ROOT, 'chemsh', 'utils', 'platforms', '__init__.py'))

    # initialise platform environment
    platfmodule.init(args)

    # YL 03/08/2023: to idendify the compiler architecture using args.__fc we must initialise these two property objects;
    #                note that we can't do this within the platfmodule.init() function which is also used by the run() function!
    # TD 22/03/2024: Important that identification of compiler architecture takes place after module load commands
    #                These are contained in platfmodule.init(args) above
    args.__CHEMSH_ARCH
    args.__FC_VERSION

    # initialise setup
    platfmodule.setup(args)

    # YL 30/11/2021: this is currently not in use but can be used later for rebuilding with same arguments, e.g., ./setup --rebuild
#    pickleArgs(args)

    # special recipes:
    args.run_tool = [ 'compile-nwchem' ]
    toolsmodule = importModule('tools', join(args.__CHEMSH_ROOT, 'chemsh', 'utils', 'setup_tools', '__init__.py'))
    toolsmodule.run(args)


def pickleArgs(args):
    '''Pickle the args'''

    import pickle
    from copy    import copy

    # save `args` for deferred use
    args_copy = copy(args)
    # delete the references to functions that cannot be pickled
    delattr(args_copy, '__parser')
    delattr(args_copy, '__get_default')
    delattr(args_copy, '__setarg')
    delattr(args_copy, '__import')
    delattr(args_copy, '__getPathOrURL')
    with open('_chemsh_build.pkl', 'wb') as fp:
        pickle.dump(args_copy, fp, 0)


# YL TODO: this is same as that in chemsh.py.in, so should keep only one (but how?)
def importModule(modname, filepath, submodules=None, **kwargs):
    '''Construct a module through its file location'''

    from importlib.util import spec_from_file_location, module_from_spec
    from os.path        import abspath, dirname, join
    from sys            import path

    # so that we can do \`from . import *\` in the package (e.g., in tools/__init__.py)
    pkgdir = abspath(join(dirname(filepath), '..'))
    if pkgdir not in path:
        path.append(pkgdir)

    spec   = spec_from_file_location(modname, filepath, submodule_search_locations=submodules)
    module = module_from_spec(spec)

    # YL 26/02/2021: allowed additional attributes via keyword arguments
    #                it has to be before exec_module() which loads everything
    for k, v in kwargs.items():
        setattr(module, k, v)

    spec.loader.exec_module(module)

    return module


def checkSubmodules():
    '''Check that any required git submodules have been collected'''

    from os import path, listdir

    # Get the directory that contains the setup script at runtime
    setup_dir = path.abspath(path.dirname(__file__))

    # Look here for the git modules list
    # (Only present for a git managed install)
    git_modules_filepath = path.join(setup_dir, '.gitmodules')

    # List of required submodules
    submodule_list = []

    # If there are submodules to fetch, they are described in this git file
    if path.exists(git_modules_filepath):
        for line in open(git_modules_filepath).readlines():
            # Collect the individual submodule paths
            if 'path' in line:
                # Read a submodule path entry (relative path)
                submodule_path = line.split()[-1]
                # Convert to absolute path
                submodule_path = path.join(setup_dir, submodule_path)
                submodule_list.append(submodule_path)

    # The existence of each submodule should be checked
    for submodule in submodule_list:

        # The directory could be missing (not a good sign)
        # This can happen when recursive is used, but the authentication fails
        if path.isdir(submodule) == False:
            print(f'\n>>> ERROR: A required git submodule is missing at {submodule}\n')
            print('You may be able to fix this with the command "git submodule update --init"')
            print('')
            print('If you are not using git, you will need to provide a copy of the missing submodule yourself')

            exit(999)

        # Normally git issues will result in an empty submodule directory
        # os.listdir gives a list of every file and dir in a path
        # if this list is empty, it is an empty directory
        elif len(listdir(submodule)) == 0:

            print(f'\n>>> ERROR: A required git submodule is missing at {submodule}\n')
            print('In order to build ChemShell it is required to use the --recursive flag when cloning the git repository')
            print('Please use the command "git clone --recursive" to avoid this error')
            print('(You may be able to fix this after the fact with the command "git submodule update --init")')
            print('')
            print('If you are not using git, you will need to provide a copy of the missing submodule yourself')

            exit(999)

    return


def pickleFF(args):
    '''Pickle CHARMM forcefield files'''

    import subprocess
    from time import time

    tstart = time()
    # Set up enviroment to run ChemShell in python3 mode
    my_env = environ.copy()
    my_env['PYTHONPATH'] = getcwd()
    if args.arch:
        my_env['CHEMSH_ARCH'] = args.arch
    else:
        my_env['CHEMSH_ARCH'] = args.__CHEMSH_ARCH
    # Get python3 exe to use
    with open('chemsh/CMakeCache.txt') as fp:
        for line in fp.readlines():
            if '_Python3_EXECUTABLE:INTERNAL=' in line:
                run_python = line.split('=')[1].rstrip()

    # Run the command
    subprocess.run([run_python, 'chemsh/data/charmm/pk_script.py'], env=my_env)
    telapsed = time() - tstart
    print("\n >>> Time used for pickling: %.3f"%telapsed, "s\n")


def main():
    ''''''

    import subprocess
    from time import time
    from os   import chdir, path, remove

    tstart = time()

    # TD 17/04/24 Check that git submodules have been provided
    checkSubmodules()

    args = parseArguments()

    # option strings of `nargs='?'` will be considered as a "switch on" if no command-line argument is present
    for key, val in args.__dict__.items():
        if val is None:
            args.__dict__[key] = True

    choosePlatform(args)

    if args.debug:
        print("\n >>> Working directory:\n", CWD)

    writeSetupLog(args, '_chemsh_build.log')

    # cd to chemsh
    chdir(path.join(path.dirname(path.realpath(__file__)), 'chemsh'))

    # remove cache variables
    try:
        remove('CMakeCache.txt')
    except:
        pass

    # clean/uninstall
    # if no argument for --clean given, run `make clean`
    if not args.clean:
        subprocess.run('make clean',
                        shell=True,
                        check=True)

    # compile/install
    else:
        # write the switch in a file, otherwise CMake will not remember it when restarting
        with open('init.txt', 'w') as fp:
            fp.write('TRUE')

        # stage 1: change the compilers only
        subprocess.run(getCMakeCmd(args, 'initialise'),
                       shell=True,
                       check=True)

        # stage 2: fully configure
        # switch off the initialising mode
        with open('init.txt', 'w') as fp:
            fp.write("FALSE")
        subprocess.run(getCMakeCmd(args),
                       shell=True,
                       check=True)

        remove('init.txt')

        if args.debug:
            print("\n >>> CMake command:\n", getCMakeCmd(args))

        # compile
        subprocess.run('make',
                        shell=True,
                        check=True)

    chdir(CWD)

    telapsed = time() - tstart
    print("\n >>> Time used for building: %.3f"%telapsed, "s\n")

    # TD 01/05/2026: For CHARMM_FF we should now pickle the FF files
    #                As the chemsh directory might be read-only past this point
    if args.charmm_ff and args.clean:
        pickleFF(args)


if __name__ == '__main__':

    main()



# TODO generate graphic dependencies
#cmake --graphviz=test.dot .

