#!python

import sys
import os
import shutil
import subprocess
from subprocess import check_call
import argparse
from argparse import ArgumentParser
from glob import glob
from logging import getLogger
from os.path import join, exists, dirname, basename
import numpy as np
import re
from top_build_cache import TopBuildCache
from tempfile import gettempdir
from random import random


oldwd = os.getcwd()

# TODO: reorder functions regarding their importance in script

def numpy_version_as_tuple() -> tuple[int]:
    """
    Converts current numpy version from str to a tuple[int].
    """
    npv = tuple([int(v) for v in np.__version__.split('.')])
    return npv


def _check_meson_tpl_missing_obj_bug():
    """
    Handles versions of python and numpy known f2py issue with meson backend.

    Ref: https://gitlab.in2p3.fr/top/top/-/issues/18
    """
    # from packaging.version import Version
    # NOTE: avoid using Version(np.__version__) < Version("2.4.1")
    #       because it is not necessarily defaultly installed in env (noticed it with conda python==3.14)
    #       don't want to add this dependency for a simple version comparison
    npv = numpy_version_as_tuple()
    if (sys.version_info >= (3, 12) and
        npv < (2, 4, 1)):
        #Version(np.__version__) < Version("2.4.1")):
        getLogger(
            'top-build'
        ).warning('Python version used is >= 3.12'
                  ' and numpy version is < 2.4.1.'
                  ' top-build is not most likely'
                  ' going to fail at linking stage.'
                  ' Please update numpy to version'
                  ' 2.4.1 or above. For example:'
                  ' type ``/root/mambaforge/envs/py311_env/bin/python3.11 -m pip install -U numpy``'
                  '.\r\n'
                  'Further info: '
                  'https://gitlab.in2p3.fr/top/top/-/issues/18')


def probe_f2py_backend() -> list[str]:
    """
    """
    if sys.version_info >= (3, 12):
        # in any case py3.12 will make recent numpy's default to meson
        # though we set the backend explicitly
        return  ['--backend', 'meson']
    else:
        # py3.11 and before
        # NOTE: backend distutils did not work with numpy==2.4.2 and setuptools==82.0.0 with py3.11
        #       a combination that worked with distutils is: numpy==1.26.4 and setuptools==59.8.0
        # TODO: so maybe setuptools version should be probed too
        npv = numpy_version_as_tuple()
        if npv[0] >= 2:
            # for major version 2 and later it is assumed
            # that the backend meson works better even with older python's
            # (according to my experience which is not exhaustive, see NOTE above)
            return  ['--backend', 'meson']
        elif npv[0] >= 1 and npv[1] >= 26:

            # for older numpy's it seems safer to run distutils
            return  ['--backend', 'distutils']
        else:
            # before 1.26 according to f2py doc
            # there was no building backend choice back then
            # (only distutils or ad-hoc compiling)
            # Ref.: https://numpy.org/doc/2.0/f2py/usage.html
            # the --backend option does not normally exist
            return ""

def get_magma_ldflags():
    magma_ldflags = "".split(';')
    if '-fopenmp' in magma_ldflags:
        # OpenMP is not handled through MAGMA flags
        # but I noticed that this flag can be found here
        # on certain configs
        magma_ldflags.remove('-fopenmp')
    if len(magma_ldflags) == 1 and magma_ldflags[0] == '':
        # empty flag list, this might certainly be a problem
        # raise at least a warning
        getLogger('top-build'
                ).warning(
                    'NO LDFLAGS FOUND BY CMAKE'
                    ' FOR MAGMA LIB:'
                    ' MAGMA won\'t work and'
                    ' most likely neither built equation.'
                )
        return []
    return magma_ldflags

def is_multi(f):
    if 'domains' in open(f).read():
        return True
    return False


def is_new_eq(f):
    if f.endswith('.eq'):
        if 'field' in open(f).read():
            return True
    return False


def is_2D(f):
    if 'llm' in open(f).read():
        return True
    return False


def is_cplx(f):
    with open(f) as eqf:
        content = eqf.read()
        return 'termi' in content or 'subi' in content


def update_quad_option(args: ArgumentParser, flags):
    """
    Updates TOP's cmake and QUADRUPLE preprocessor configurations with
    the actual quadruple configuration (enabled or disabled) asked
    by user in ArgumentParser.

    The update uses cmake if the update is needed.
    """

    if args.quad:
        flags += ' -DQUADRUPLE=2'  # all quad except LU
        # NOTE: consistency with cmake possible call below
        cmake_quad_up2date = '2' == '0'
    elif args.double:
        flags += ' -DQUADRUPLE=0'
        cmake_quad_up2date = '0' == '0'
        # args.quad == False
    else:
        # double or quadruple used implicitly depending on cmake conf
        flags += " -DQUADRUPLE=0"
        # top-build previously configured QUADRUPLE option is used
        # no change, just return
        return flags

    wd_before_quad = os.getcwd()
    os.chdir('/builds/top/top/build')

    # TODO: if quad or double configuration asked is available in cache
    # it should not be necessary to rebuild the project
    # especially true and even unavoidable if the project is not available
    # (e.g. in a pip/conda package)
    if not cmake_quad_up2date:
        # NOTE: it is assumed that top-build is consistent with the latest cmake run
        default_quad = re.match(r'.*=2$', flags) is not None
        if args.debug:
            print("cmake_quad_up2date:",
                  cmake_quad_up2date, 'quad:', args.quad,
                  "default_quad:", default_quad)
        # use cmake to configure core TOP code and dependencies
        # NOTE: make no confusion between QUADRUPLE cmake option (ON|OFF)
        # and QUADRUPLE compiling preprocessor constant (0|1|2)
        # with 1 or 2 choice depending of ALL_QUAD_EXCEPT_LU
        if default_quad:
            # --quad option has been used or we wouldn't be here
            # so only ALL_QUAD_EXCEPT_LU=ON is used
            subprocess.check_output("cmake -DQUADRUPLE=ON "
                                    "-DALL_QUAD_EXCEPT_LU=ON"
                                    " -UEQUATION -UMODEL ..".split(),
                                    stderr=subprocess.STDOUT)
            # NOTE: consistency with preprocessor QUADRUPLE set above
        else:
            subprocess.check_output("cmake -DQUADRUPLE=OFF"
                                    "  -UEQUATION -UMODEL ..".split(),
                                    stderr=subprocess.STDOUT)

        subprocess.check_output("cmake --build . -t install".split(),
                                stderr=subprocess.STDOUT)
    os.chdir(wd_before_quad)

    return flags


def main():
    # TODO: add --cache <dir> option (disabled by default, dir maybe later in ini).
    # TODO: add to cache .mod, .so and possible headers from tierce libraries
    # TODO: if requested, get sources from cache (python.F90, config.F90 and maybe others)
    # TODO: handle magma in cache

    cmake_build_avail = exists('/builds/top/top/build')
    # TODO: take care that the directory might exist but can be defunct

    _check_meson_tpl_missing_obj_bug()

    parser = argparse.ArgumentParser()

    parser.add_argument('--cplx',
                        dest='cplx_opt', action='store_true',
                        help='Forces usage of complex version')

    parser.add_argument('--quad',
                        action='store_true',
                        default=False,
                        help='Use quadruple precision for TOP Arnoldi solver'
                        ' and embedded libraries (LAPACK, BLAS, etc.) except'
                        ' for LU that stays in double precision (this'
                        ' suboption can only be disabled through cmake).')

    parser.add_argument('--double',
                        action='store_true',
                        default=False,
                        help='Enable double precision for TOP.'
                        ' This is default behaviour, option is'
                        ' only used to disable'
                        ' quadruple precision explicitly (because it has been'
                        ' set from cmake externally).')
                        # NOTE: that's why default is and must be kept False

    # parser.add_argument('--parser',
    #         dest='new_parser', action='store_true',
    #         help='Use new parser')

    # TODO: implement this option (mod_blacs or ScaLAPACK etc.)
    parser.add_argument('--mpi',
                        action='store_true',
                        default=False,
                        help='Enable MPI (default: disabled)')

    parser.add_argument('file',
                        help='equation file')

    parser.add_argument('--order',
                        nargs=1,
                        help='use an alternative `order.inc\' file')

    parser.add_argument('--model',
                        required=True,
                        help='input model (ester or poly_ester, etc)')

    parser.add_argument('--debug', dest='debug', action='store_true',
                        help='debug')
    parser.set_defaults(debug=False)

    parser.add_argument('--latex',
                        nargs=1,
                        help='output LaTeX file')

    parser.add_argument('--pyreadeq',
                        action='store_true',
                        default=False,
                        help='use Python implementation readeq for equation '
                        'legacy format. Note that this implementation is used '
                        'anyway if anything prevents Perl readeq scripts to '
                        'work.')

    tmpdir = join(gettempdir(), f'top-build_{random()}')
    os.makedirs(tmpdir, exist_ok=True)
    print(f"Created temporary top-build working directory {tmpdir}")

    args = parser.parse_args()

    eqfile = args.file

    # NOTE: the order below matches dependencies
    # a module's dependencies must appear before the considered module
    # Dependency means that for one module to be built the .mod
    # of the dependency is needed
    # TODO: factorize mods_info initialization in a loop for similar modules
    # TODO: or this dict could be replaced by a .ini to parse (in which it should be
    # clearly mentioned what options are mandatory and which ones are not)
    mods_info = {'grid': {'src_ppath': '/builds/top/top/src/',
                          'mod_ppath': tmpdir, #'/builds/top/top/build/src',
                          'obj_ppath': tmpdir, # '/builds/top/top/build/src/',
                          'mod_name': 'mod_grid',
                          'obj_name': 'grid',
                          'src_ext': 'F90'},
                 # TODO: inputs ppath should be where readeq generated them
                 # no need of the change dir above, or rather go in temp dir
                 'inputs': {'src_ppath': tmpdir, #'/builds/top/top/build/src/',
                            'mod_ppath': tmpdir, #'/builds/top/top/build/src',
                            'obj_ppath': tmpdir, #'/builds/top/top/build/src/',
                            'mod_name': 'inputs',
                            'obj_name': 'inputs',
                            'src_ext': 'F90',
                            'obj_to_link': False,
                            'mod_to_compile': True,
                            'f2py_wrapper': True,
                            'use_cache_mod': False,
                            'use_cache_src': False},
                 # inputs never got from cache because it is highly
                 # dependent to compiled equation file
                 # (for fields set  with keyword input)
                 'model': {'src_ppath': '/builds/top/top/build/src/',
                           'mod_ppath': tmpdir, #'/builds/top/top/build/src',
                           'obj_ppath': tmpdir, #'/builds/top/top/build/src/',
                           'mod_name': 'model',
                           'obj_name': 'model',
                           'src_ext': 'F90'},
                 'config': {
                     'src_ppath': '/builds/top/top/src/',
                     'mod_ppath': '/builds/top/top/build/src',
                     'obj_ppath': '/builds/top/top/build/src/CMakeFiles/src_lib.dir',
                     'mod_name': 'cfg',
                     'obj_name': 'config',
                     'src_ext': 'F90',
                     'to_compile': False, # already done by CMake
                     'obj_to_link': False, # no or duplicated symbols will occur with f2py_wrapper
                     'f2py_wrapper': True,
                     'use_cache_mod': True,
                     'use_cache_src': True
                 },
                 'matrices': {'src_ppath': '/builds/top/top/src/',
                              'mod_ppath': tmpdir, #'/builds/top/top/build/src',
                              'obj_ppath': tmpdir, #'/builds/top/top/build/src/',
                              'mod_name': 'matrices',
                              'obj_name': 'matrices',
                              'src_ext': 'F90',
                              'use_cache_mod': False, # TODO: reenable when matrices.inc and order.inc will be their own modules
                              'use_cache_obj': False,
                              'use_cache_src': True},
                                                   # (no more included in matrices.F90)
                 'eigensolve': {'src_ppath': '/builds/top/top/src/',
                                'mod_ppath': tmpdir,  # '/builds/top/top/build/src',
                                'obj_ppath': tmpdir,  # '/builds/top/top/build/src/',
                                'mod_name': 'eigensolve',
                                'obj_name': 'eigensolve',
                                'src_ext': 'F90'},
                 'postproc': {'src_ppath': '/builds/top/top/src/',
                              'mod_ppath': tmpdir,  # '/builds/top/top/build/src',
                              'obj_ppath': tmpdir,  # '/builds/top/top/build/src/',
                              'mod_name': 'postproc',
                              'obj_name': 'postproc',
                              'src_ext': 'F90'},
                 'mod_blacs': {
                     'src_ppath': '/builds/top/top/src/', # irrelevant, no compiling (done by CMake)
                     'mod_ppath': '/builds/top/top/build/src/',
                     'obj_ppath': '/builds/top/top/build/src/CMakeFiles/src_lib.dir/',
                     'obj_to_link': True,
                     'to_compile': False,  # already done by cmake
                     'mod_name': 'mod_blacs',
                     'obj_name': 'mod_blacs.F90',  # cmake makes a <file>.F90.o object
                     'use_cache_mod': False,  # need obj in cache
                     'use_cache_obj': True  # need obj in cache
                 },
                 'abstract_model': {
                     'src_ppath': '/builds/top/top/src/', # irrelevant, no compiling (done by CMake)
                     'mod_ppath': '/builds/top/top/build/src/',
                     'obj_ppath': '/builds/top/top/build/src/CMakeFiles/src_lib.dir/',
                     'obj_to_link': True,
                     'to_compile': False,
                     'mod_name': 'abstract_model_mod',
                     'obj_name': 'abstract_model.F90', # cmake makes a <file>.F90.o object
                 },
                 'ester-interface': {
                     'src_ppath': '/builds/top/top/src/', # irrelevant, no compiling (done by CMake)
                     'mod_ppath': '/builds/top/top/build/src', # not relevant (not fortran)
                     'obj_ppath': '/builds/top/top/build/src/CMakeFiles/src_lib.dir/',
                     'obj_to_link': True,
                     'to_compile': False, # already done by cmake
                     'mod_name': 'ester-interface', # TODO: not used
                     'obj_name': 'ester-interface.cpp', # cmake makes a <file>.cpp.o object
                     'use_cache_mod': False,  # need obj in cache
                     'use_cache_obj': True  # need obj in cache
                 },
                 'hpsort_index': {
                     'src_ppath': '/builds/top/top/src/', # irrelevant, no compiling (done by CMake)
                     'mod_ppath': '/builds/top/top/build/src/', # irrelevant (not a fortran module)
                     'obj_ppath': '/builds/top/top/build/src/CMakeFiles/src_lib.dir/',
                     'obj_to_link': True,
                     'to_compile': False, # already done by cmake
                     'mod_name': 'hpsort_index', # not used and possibly wrong
                     'obj_name': 'hpsort_index.F90', # cmake makes a <file>.F90.o object
                     'use_cache_mod': False,  # need obj in cache
                     'use_cache_obj': True  # need obj in cache 
                 },
                 'python': {
                     'src_ppath': '/builds/top/top/src/',
                     'mod_ppath': '/builds/top/top/build/src', # TODO: not relevant
                     'obj_ppath': '/builds/top/top/build/src/', # TODO: not relevant
                     'mod_name': 'python', # TODO: not relevant
                     'obj_name': 'python',
                     'src_ext': 'F90',
                     'to_compile': False, # f2py wrapper
                     'obj_to_link': False,
                     'f2py_wrapper': True,
                     'use_cache_src': True,
                     'use_cache_mod': False,
                     'use_cache_obj': False
                 }
                 }

    tb_cache = TopBuildCache(mods_info, args.debug)

    if not os.path.isfile(eqfile):
        print('Error: file `' + eqfile + '\' does not exists as file')
        sys.exit(1)

    if not args.pyreadeq and not is_new_eq(eqfile):
        try:
            subprocess.check_output(['/root/mambaforge/envs/py311_env/bin/perl', '-v'])
        except:
            print('Warning: perl not found by cmake, falling back to python '
                  'readeq for equation legacy format (--pyreadeq option).')
            args.pyreadeq = True

    # get abs path for equation file
    eqfile = dirname(os.path.abspath(eqfile)) \
        + '/' + os.path.basename(eqfile)

    # if args.new_parser:
    #     readeq = '/builds/top/top/build/parser/readeq'
    # else:
    #     readeq = '/builds/top/top/build/utils/readeq'

    flags = ''
    f2py_def = ''

    readeq = ''

    if is_cplx(eqfile) or args.cplx_opt:
        flags += ' -DUSE_COMPLEX'
        f2py_def = '-DUSE_COMPLEX'
        tb_cache.add_conf_opt('complex')
    else:
        f2py_def = '-DUSE_REAL'
        tb_cache.add_conf_opt('real')

    if is_2D(eqfile):
        tb_cache.add_conf_opt('2D')
    else:
        flags += ' -DUSE_1D'
        f2py_def += ' -DUSE_1D'
        tb_cache.add_conf_opt('1D')

    if is_multi(eqfile):
        flags += ' -DUSE_MULTI'
        f2py_def += ' -DUSE_MULTI'
        tb_cache.add_conf_opt('multi')
    else:
        tb_cache.add_conf_opt('mono')

    if not is_new_eq(eqfile):

        if args.latex:
            print('Warning: LaTeX output not supported for legacy equation'
                  ' format')
            print('Ignoring --latex option...')

        if args.pyreadeq:
            readeq = '/builds/top/top/build/utils/readeq.py'
            if not exists(readeq):
                for re_dir in os.environ['PATH'].split(':'): # TODO: handle portable env path sep
                    # try to find it in env PATH
                    readeq = join(re_dir, 'readeq.py')
                    if exists(readeq):
                        break
            if args.debug:
                print("readeq.py found:", readeq)
        else:
            # perl readeq
            readeq = '/builds/top/top/build/utils/readeq'

            if is_cplx(eqfile) or args.cplx_opt:
                readeq += '-cplx'
            else:
                readeq += '-real'

            if is_2D(eqfile):
                readeq += '-2D'
            else:
                readeq += '-1D'

            if is_multi(eqfile):
                readeq += '-multi'
            else:
                readeq += '-mono'
    else:
        if args.pyreadeq:
            print('Warning: pyreadeq supports only legacy equation format'
                  ' (ignoring option --pyreadeq)')

    if args.quad and args.double:
        raise ValueError('Cannot use --quad and --double at the same time')

    if exists('/builds/top/top/build'):
        flags = update_quad_option(args, flags)
        # check flags preprocessor flags to determine
        # if we enable quad or not and in which conf (1 or 2)
        quad = re.match(r'.*=1$|.*=2$', flags) is not None
    else:
        quad = args.quad
        if quad:
            flags += " -DQUADRUPLE=2" # equivalent to cmake -DQUADRUPLE=ON -DALL_QUAD_EXCEPT_LU=ON
        else:
            flags += " -DQUADRUPLE=0" # equivalent to cmake DQUADRUPLE=OFF


    if args.debug:
        print("cache will look for config:", f"quad={quad}", f"(flags={flags})")

    # update cache configuration consistently with quad/double config
    if quad:
        tb_cache.add_conf_opt('quad', prepend_as_parent_folder=True)
    else:
        tb_cache.add_conf_opt('double', prepend_as_parent_folder=True)

    flags += " -DARNOLDI_TIME=0"

    if args.model == 'ester':
        flags += ' -DUSE_LIBESTER'

    tb_cache.add_conf_opt(args.model)

    if is_new_eq(eqfile):
        readeq = '/builds/top/top/build/../parser/build/bin/readeq'

    if args.order:
        order = args.order[0]
        if not os.access('%s' % order, os.F_OK):
            print('Error: no such file `%s\'' % order)
            sys.exit(1)
    else:
        if is_2D(eqfile):
            order = '/builds/top/top/src/order-default.inc'
        else:
            order = '/builds/top/top/src/order-default-1D.inc'

    shutil.copy(order,
                join(tmpdir, 'order.inc'))
    # '/builds/top/top/build/src/order.inc')


    if exists('@CMAKE_SOURCE_DIR'):
        if not os.access('/builds/top/top/src/model_%s.F90' % args.model, os.F_OK):
            print('Error: no model named `%s\'' % args.model)
            sys.exit(1)
    #else: # source not available, TODO: cache should be consulted to know if a certain model exists


    # copy the model asked as model.F90 (will be built by F2PY)
    # TODO: not necessary if overridden later
    if exists('/builds/top/top/build/src/model.F90'):
        os.remove('/builds/top/top/build/src/model.F90')

    if exists('/builds/top/top'):
        shutil.copy('/builds/top/top/src/model_%s.F90' % args.model,
                    '/builds/top/top/build/src/model.F90')
    # else: cmake src dir does not exist
    # (maybe top-build was installed from python pkg
    # in that case the copy is unneeded)

    if not os.path.isfile(readeq):
        print('Error incompatible options (readeq: `%s\')' % readeq)
        sys.exit(1)

    # TODO: use print(*args, end='') instead of sys.stdout.write
    if args.debug:
        sys.stdout.write('Building [DEBUG] ' + os.path.basename(eqfile) + ' (')
    else:
        sys.stdout.write('Building ' + os.path.basename(eqfile) + ' (')

    if is_cplx(eqfile) or args.cplx_opt:
        sys.stdout.write('complex ')
    else:
        sys.stdout.write('real ')

    if is_2D(eqfile):
        sys.stdout.write('2D ')
    else:
        sys.stdout.write('1D ')

    if is_multi(eqfile):
        sys.stdout.write('multi)')
    else:
        sys.stdout.write('mono)')

    sys.stdout.write('... ')
    sys.stdout.flush()

    try:
        # TODO: refactor the following code into suited functions
        if 'OFF' == 'OFF':
            magma_include = '-I.'
        else:
            magma_include = '-I.'
            # use same include dirs as in project root CMakeLists.txt
            if '' != '':
                magma_include += ' -I'
            elif '' != '':
                magma_include += ' -I'
            magma_include += ' -I/builds/top/top/src'
            magma_include += ' -DUSE_MAGMA=1'
            magma_include += ' '+' '.join(''.split(';'))
            magma_include = magma_include.replace('-fopenmp', '') # fix bad use of -fopenmp flag that might happen with certain confs (e.g. gfortran+magma+openblas)

        # remove all previous *.mod to avoid interferences between two different models/equations
        # TODO: it should not be necessary when top-build is used in a row on the same model/eq. type

        libname = os.path.basename(eqfile).split('.')[0]
        # os.chdir('/builds/top/top/build/src')
        os.chdir(tmpdir)
        mods_to_del = ['matrices.inc', 'mod_grid.mod', 'matrices.mod',
                       'inputs.mod', 'eigensolve.mod', 'model.mod',
                       # , 'mod_blacs.mod'
                       'postproc.mod']

        for mod in mods_to_del: # not glob("*.mod") because don't want to delete cmake pre-built modules
            try:
                os.remove(mod)
                if args.debug:
                    print("removed", mod)
            except:
                # most likely the file wasn't created before
                pass

        if is_new_eq(eqfile):
            cmd = readeq + ' -o matrices.inc'
            if is_2D(eqfile):
                cmd += ' -d 2 '
            else:
                cmd += ' -d 1 '
            if args.latex:
                cmd += ' -l ' + oldwd + '/' + args.latex[0] + ' '
            cmd += eqfile
            subprocess.check_output(cmd.split(),
                                    stderr=subprocess.STDOUT)
        elif args.pyreadeq:
            sys.path.append('/builds/top/top/build/utils/')
            from readeq import main as readeq_main
            readeq_main(['readeq.py', eqfile] + (['--cplx'] if args.cplx_opt else []))
        else:
            subprocess.check_output(['/root/mambaforge/envs/py311_env/bin/perl', readeq, eqfile],
                                    stderr=subprocess.STDOUT)



        # TODO: get_f2py_cmd function
        # in priority use f2py set by cmake
        # if unavailable use f2py or f2py3 from PATH
        if exists('/builds/top/top/build') and exists("/root/mambaforge/envs/py311_env/bin/f2py"):
            f2py_cmd = "/root/mambaforge/envs/py311_env/bin/f2py"
        else:
            with open(join(tmpdir, 'check_f2py_out_err'), 'w') as cfoe:
                exec_err_msg = "Failed to execute {f2py_cmd}."
                try:
                    f2py_ret = check_call(['f2py', '-h'], stderr=cfoe, stdout=cfoe)
                    f2py_cmd = "f2py"
                    if f2py_ret:
                        print(exec_err_msg.format(f2py_cmd=f2py_cmd))
                except:
                    try:
                        f2py3_ret = check_call(['f2py3', '-h'], stderr=cfoe, stdout=cfoe)
                        f2py_cmd = "f2py3"
                        if f2py3_ret:
                            print(exec_err_msg.format(f2py_cmd=f2py_cmd))
                    except:
                        print("Failed to find from CMake configuration or f2py or f2py3 in the PATH environment variable")
                        exit(2)
                        # TODO: --f2py option to set arbitrary f2py path
        if args.debug:
            print("F2PY used:", f2py_cmd)


        # same for fortran compiler
        # use cmake compiler if most likely running the script on building host (cmake project exists)
        # otherwise, or if compiler not found, fall back to gfortran
        # but still don't override FC environnement variable that has the last word
        # TODO: indicate in doc that the compiler can be changed with FC env var
        #       same CC can change C compiler
        # TODO: do this in a dedicated function
        if exists('/builds/top/top/build') and exists("/root/mambaforge/envs/py311_env/bin/gfortran") and 'FC' not in os.environ:
            fc = "/root/mambaforge/envs/py311_env/bin/gfortran"
        else:
            # NOTE: FC is consulted by f2py/meson/distutils (not sure for meson)
            # but FC might be used to compile modules independently from f2py,
            # so the fallback below from FC env. var.
            if 'FC' in os.environ:
                fc = os.environ['FC']
            else:
                fc = 'gfortran'
            with open(join(tmpdir, f'check_{basename(fc)}'), 'w') as cfe:
                try:
                    fc_ret = check_call([fc, '-v'], stderr=cfe, stdout=cfe)
                    # TODO: check that ifx supports -v (I think so, ifort and gfortran does)
                    # that just to avoid error status
                except:
                    print(f"Failed to fall back to {basename(fc)} compiler.")
                    exit(3)


        if args.debug:
            print("Fortran compiler used:", fc)

        tbc_state = tb_cache.get_cache_state()

        # The following compiling is just to get .mod from Fortran compiler
        # next f2py will know the modules it needs
        # it implies then other dependencies due to the modules to compile
        # because the modules are compiled it makes sense to keep object files and give them to F2PY
        # (this way F2PY does not compile them again)
        # TODO: use standard python tools to build them with the final extension
        for mod in tbc_state['mods_to_compile']:

            build_mod_cmd = [fc] + ([f'-I{(tb_cache.get_mod_dir())}'  # path top's library modules in cache
                                 ] if exists(tb_cache.get_mod_dir()) else [ # path top's library modules in cache
                                 ]) + tbc_state['mod_paths_to_inc_f2py'] + [ # cache path is prioritary
                                '-I/builds/top/top/build/src', # for previously generated .mod
                                '-I/builds/top/top/build/lib/getpar/',
                                '-I/builds/top/top/src', # for real_kind.h
                                '-I/builds/top/top', # for config.h
                                '-I/builds/top/top/build/lib/der',
                                '-I/builds/top/top/build/lib/itemlist',
                                '-I/builds/top/top/build/lib/legendre',
                                '-I/builds/top/top/build/lib/getpar',
                                '-I/builds/top/top/build/lib/der',
                                '-ffree-line-length-none', # no limit for fortran line length (error happened with model_cesam.F90)
                                # TODO: this gfortran's flag, should be more portable (e.g.: ifort/ifx)
                                '-fPIC',
                                '-cpp',
                                *magma_include.split(),
                                *flags.split(),
                                '-c', mod]


            if args.debug:
                print(" ".join(build_mod_cmd))

            subprocess.check_output(build_mod_cmd,
                                    stderr=subprocess.STDOUT)


        os.chdir(oldwd)

        # NOTE: that legpy.so is pre-built by CMake
        # TODO: it should be made here as other extra f2py wrappers (e.g. magma_import)

        # TODO: use os.path.sep or os.path.join for path portability
        # TODO: set_ldflags(args) function to refactor all this
        if exists('/builds/top/top/build'):
            os.makedirs('/builds/top/top/build/src/lib', exist_ok=True)

        if '-L.;-L/usr/local/lib/' == '':
            ldflags = '-L.'
        else:
            ldflags = '-L.;-L/usr/local/lib/'.replace(';', ' ')

        # complete ldflags with other libraries/libpaths:
        ldflags = ldflags.split() + [
                  '-L/root/local/lib',
                  # TODO: remove unnecessary libs
                  # (most likely already in ldflags)
                  '-lgetpar',
                  '-llegendre',
                  '-larnoldi',
                  '-lder',
                  '-llapack_extra']

        if args.mpi:
            ldflags += ['-litemlist']
            # itemlist dep happens only for perl readeq-cplx-2D-multi-mpi
            # NOTE: so far mpi option is not implemented in top-build

        if args.model == 'cesam' and is_2D(eqfile):
            # for interpolate, interpolate_derive fortran subroutines
            # only used with cesam 2D models (see src/model_cesam.F90)
            ldflags += ['-lfit']

        if args.model == 'ester':
            ldflags += ['-lester']

        if quad:
            # true_dgetrf needed (for -DQUADRUPLE=2 <=> cmake -DALL_QUAD_EXCEPT_LU)
            if exists('/builds/top/top/build'):
                ldflags += ['-L/builds/top/top/build/lib_q/blas_q',
                            '-L/builds/top/top/build/lib_q/lapack_q',
                            '-L/builds/top/top/build/lib_q/dgetrf']
            ldflags += ['-lblas_q', '-llapack_q', '-ldgetrf']


        if 'OFF' != 'OFF':
            ldflags += get_magma_ldflags()

        # generate .f2py_f2cmap needed for quadruple floating point
        f2cmap_fp = join(tmpdir, '.f2py_f2cmap')
        with open(f2cmap_fp, mode='w') as f2cmap:
            print("{'real': {'c_long_double': 'long_double'}}", file=f2cmap, sep='')


        if args.debug:
            # debug mode
            # output f2py-generated source code before building it!
            src_gen_cmd = [f2py_cmd,
                           # NOTE: default f2cmap is generated before, specified explicitly because in tmpdir and not in cwd
                           '--f2cmap',
                           f2cmap_fp,
                           #            '/builds/top/top/src/f2py_typemap.txt',
                           "-m", libname] + tbc_state['mods_to_f2py_wrap']
            #            "/builds/top/top/src/python.F90",
            #            " /builds/top/top/src/config.F90",
            #]
            subprocess.check_output(src_gen_cmd, stderr=subprocess.STDOUT)
            print("generated wrapper C/fortran source :"+ ", ".join(glob(libname+"*.c")+glob(libname+"*.f90")))

        if exists('/builds/top/top/build'):
            cmake_incs =  [
                '-I/builds/top/top/build',
                '-I/builds/top/top/build/src',
                '-I/builds/top/top/build/lib/itemlist',
                '-I/builds/top/top/build/lib/legendre',
                '-I/builds/top/top/build/lib/getpar',
                '-I/builds/top/top/build/lib/der',
                '-I/builds/top/top/src',
                # '--debug',
            ]
        else:
            cmake_incs = []


        objs_to_link = tbc_state['objs_to_link']

        if args.model != 'ester':
            objs_to_link = [obj for obj in objs_to_link if 'ester-interface' not in obj]

        cmd = [f2py_cmd,
               *probe_f2py_backend(),
               # NOTE: default f2cmap is generated before and specified explicitly because in tmpdir and not in cwd
               '--f2cmap',
               f2cmap_fp,
               #'/builds/top/top/src/f2py_typemap.txt',
               f'--f90exec={fc}',
               f'--f77exec={fc}',
               f'--f90flags=-cpp -fopenmp {flags}',  # -ggdb -fcheck=all'
               f'--f77flags=-cpp -fopenmp {flags}',  # -ggdb -fcheck=all'
              ] + ([f'-I{tb_cache.get_mod_dir()}'  # path top's library modules in cache
                  ] if exists(tb_cache.get_mod_dir()) else [
              ]) + tbc_state['mod_paths_to_inc_f2py'  # cache inc paths are prioritary
                           ] + cmake_incs + magma_include.split() + [
                  f'-L{tb_cache.get_lib_dir()}'
                  # set priority to cache libraries if available
                  ] + [
                  *ldflags,
                  *f2py_def.split(),
                  '-c',
                  '-m',
                  libname] +  \
                objs_to_link + tbc_state['mods_to_f2py_wrap']

        if args.debug:
            print(" ".join(cmd))
        # print("cmd=", '\n', " ".join(cmd))
        subprocess.check_output(cmd,
                                stderr=subprocess.STDOUT)

        if 'OFF' != 'OFF':
            subprocess.check_output(
                [f2py_cmd, f'--f90exec={fc}', f'--f77exec={fc}', *probe_f2py_backend()] +
                magma_include.split() +
                [
                   f'--f77flags=-cpp -fopenmp {flags}',  # -ggdb -fcheck=all'
                   f'--f90flags=-cpp -fopenmp {flags}',  # -ggdb -fcheck=all'
                    *ldflags,
                    *f2py_def.split(),
                    '-c',
                    '-m',
                    'magma_import'] +
                ['/builds/top/top/src/magma_interface.F90'],
                stderr=subprocess.STDOUT)

        # TODO: do not use ls, use glob
        files = subprocess.check_output(['ls']).decode().split()
        for f in files:
            if libname in f and f.endswith('.so'):
                outlibname = f
                break

        if args.debug:
            print("output:", outlibname)

        if exists('/builds/top/top/build'): # TODO: a variable using_cmake should be added for safety
                                         # because it can be that the directory exists but still
                                         # we are not from cmake project

            if args.model == 'ester':
                libs_to_cache_anyway = ['ester']
            else:
                libs_to_cache_anyway = None
            # cache effectively linked libraries
            tb_cache.cache_top_libs(outlibname,
                                    search_lib_paths=[join('/builds/top/top/build', 'lib'),
                                                      join('/builds/top/top/build', 'lib_q')],
                                    add_mods=True,
                                    libs_to_cache_anyway=libs_to_cache_anyway)
            # add manually legpy wrapper (to legendre lib)
            # because it is not necessarily linked to equation wrapper
            # TODO: use .dylib extension if running macOS X
            # TODO: this wrapper should be added like this because it is proper
            # to the python version used. Either we don't add it or we generate it here
            # (this is what's done for magma_import wrapper above)
            legpy_path = join('/root/local', 'lib', 'legpy.so')
            if exists(legpy_path):
                tb_cache.cache_top_libs(legpy_path, cache_lib_itself=True)

            if args.model == 'ester':
                # ester has its own dependencies that must be included
                # get ester lib filepath and add its relevant dependencies into cache
                # TODO: a dedicated transparent function should be defined instead of
                # accessing cache state directly
                ester_lib_path = [lib[0] for lib in tb_cache.state['files_to_cache'] if 'libester.so' in lib[0]]
                assert len(ester_lib_path) > 0
                if args.debug:
                    print("ester_lib_path:", ester_lib_path)
                tb_cache.cache_top_libs(ester_lib_path[0], cache_lib_itself=False, # ester is already in cache
                                        # but not these dependencies, embedded in the ester project
                                        # include them in cache:
                                        libs_to_cache_anyway=['opint', 'cesam', 'freeeos']
                                       )
            # the following copy and rename remove cpython version suffix
            # TODO: we should stop doing that,
            # knowing the python version is useful at import time
            os.makedirs('/root/local/lib/', exist_ok=True)
            # directory might not pre-exist if top-build is used
            # from CMake before folder creation (make-install)
            shutil.copy(outlibname,
                        '/root/local/lib/' + libname + '.so')
            shutil.move(outlibname, libname + '.so')

            if args.debug:
                print("copied in install dir.:",
                      '/root/local/lib/' + libname + '.so')

            tb_cache.dump_cache()
        # else: not in cmake project so we don't do all this

        print('[OK]')
        sys.exit(0)

        # TODO: before exiting delete tmpdir if not in debug mode

    except subprocess.CalledProcessError as e:
        print('[Failed]')
        os.chdir(oldwd)
        f = open('top-build.err', 'w')
        print('Check `top-build.err\'')
        try:
            f.write(e.output.decode())
        except:
            f.write(e.output)
        f.close()
        print(e)
        sys.exit(1)

    except Exception as e:
        print(e)
        sys.exit(1)


if __name__ == '__main__':
    main()
