import os
import sys
import setuptools
import pathlib
from setuptools import Extension
from setuptools.command.build_ext import build_ext
from setuptools.command.install import install
import subprocess
try:
    from wheel.bdist_wheel import bdist_wheel
    WHEEL_INSTALLED = True
except:
    #wheel package not installed.
    WHEEL_INSTALLED = False
    pass

# Singleton class that holds the settings configured using command line
# options. This information has to be stored in a singleton so that it
# can be passed between different stages of the build, and because pip
# has strange behavior between different versions.
class CL_opt:
    instance = None
    def __init__(self):
        if not CL_opt.instance:
            CL_opt.instance = {'mpi': False,
                               'gpu': 'none',
                               'vec': False,
                               'arch': 'none',
                               'neuroml': True,
                               'bundled': True}

    def settings(self):
        return CL_opt.instance

def cl_opt():
    return CL_opt().settings()

# extend user_options the same way for all Command()s
user_options_ = [
        ('mpi',   None, 'enable mpi support (requires MPI library)'),
        ('gpu=',  None, 'enable nvidia cuda support (requires cudaruntime and nvcc) or amd hip support. Supported values: '
                        'none, cuda, cuda-clang, hip'),
        ('vec',   None, 'enable vectorization'),
        ('arch=', None, 'cpu architecture, e.g. haswell, skylake, armv8.2-a+sve, znver2 (default native).'),
        ('neuroml', None, 'enable parsing neuroml morphologies in Arbor (requires libxml)'),
        ('sysdeps', None, 'don\'t use bundled 3rd party C++ dependencies (pybind11 and json). This flag forces use of dependencies installed on the system.')
    ]

# VERSION is in the same path as setup.py
here = os.path.abspath(os.path.dirname(__file__))
with open(os.path.join(here, 'VERSION')) as version_file:
    version_ = version_file.read().strip()

# Get the contents of the readme
with open(os.path.join(here, 'python/readme.md'), encoding='utf-8') as f:
    long_description = f.read()

def check_cmake():
    try:
        out = subprocess.check_output(['cmake', '--version'])
        return True
    except OSError:
        return False


class _command_template:
    """
    Override a setuptools-like command to augment the command line options.
    Needs to appear before the command class in the class's argument list for
    correct MRO.

    Examples
    --------

    .. code-block: python

      class install_command(_command_template, install):
          pass


      class complex_command(_command_template, mixin1, install):
          def initialize_options(self):
              # Both here and in `mixin1`, a `super` call is required
              super().initialize_options()
              # ...

    """
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.user_options = super().user_options + user_options_


    def initialize_options(self):
        super().initialize_options()
        self.mpi  = None
        self.gpu  = None
        self.arch = None
        self.vec  = None
        self.neuroml = None
        self.sysdeps = None

    def finalize_options(self):
        super().finalize_options()

    def run(self):
        # The options are stored in global variables:
        opt = cl_opt()
        #   mpi  : build with MPI support (boolean).
        opt['mpi']  = self.mpi is not None
        #   gpu  : compile for AMD/NVIDIA GPUs and choose compiler (string).
        opt['gpu']  = "none" if self.gpu is None else self.gpu
        #   vec  : generate SIMD vectorized kernels for CPU micro-architecture (boolean).
        opt['vec']  = self.vec is not None
        #   arch : target CPU micro-architecture (string).
        opt['arch'] = 'none' if self.arch is None else self.arch
        #   neuroml : compile with neuroml support for morphologies.
        opt['neuroml'] = self.neuroml is not None
        #   bundled : use bundled/git-submoduled 3rd party libraries.
        #             By default use bundled libs.
        opt['bundled'] = self.sysdeps is None

        super().run()


class install_command(_command_template, install):
    pass

if WHEEL_INSTALLED:
    class bdist_wheel_command(_command_template, bdist_wheel):
        pass


class cmake_extension(Extension):
    def __init__(self, name):
        Extension.__init__(self, name, sources=[])


class cmake_build(build_ext):
    def run(self):
        if not check_cmake():
            raise RuntimeError('CMake is not available. CMake 3.12 is required.')

        # The path where CMake will be configured and Arbor will be built.
        build_directory = os.path.abspath(self.build_temp)
        # The path where the package will be copied after building.
        lib_directory = os.path.abspath(self.build_lib)
        # The path where the Python package will be compiled.
        source_path = build_directory + '/python/arbor'
        # Where to copy the package after it is built, so that whatever the next phase is
        # can copy it into the target 'prefix' path.
        dest_path = lib_directory + '/arbor'

        opt = cl_opt()
        cmake_args = [
            '-DARB_WITH_PYTHON=on',
            '-DPYTHON_EXECUTABLE=' + sys.executable,
            '-DARB_WITH_MPI={}'.format( 'on' if opt['mpi'] else 'off'),
            '-DARB_VECTORIZE={}'.format('on' if opt['vec'] else 'off'),
            '-DARB_ARCH={}'.format(opt['arch']),
            '-DARB_GPU={}'.format(opt['gpu']),
            '-DARB_WITH_NEUROML={}'.format( 'on' if opt['neuroml'] else 'off'),
            '-DARB_USE_BUNDLED_LIBS={}'.format('on' if opt['bundled'] else 'off'),
            '-DCMAKE_BUILD_TYPE=Release' # we compile with debug symbols in release mode.
        ]

        print('-'*5, 'command line arguments: {}'.format(opt))
        print('-'*5, 'cmake arguments: {}'.format(cmake_args))

        build_args = ['--config', 'Release']

        # Assuming Makefiles
        build_args += ['--', '-j2']

        env = os.environ.copy()
        env['CXXFLAGS'] = '{}'.format(env.get('CXXFLAGS', ''))
        if not os.path.exists(self.build_temp):
            os.makedirs(self.build_temp)

        # CMakeLists.txt is in the same directory as this setup.py file
        cmake_list_dir = os.path.abspath(os.path.dirname(__file__))
        print('-'*20, 'Configure CMake')
        subprocess.check_call(['cmake', cmake_list_dir] + cmake_args,
                              cwd=self.build_temp, env=env)

        print('-'*20, 'Build')
        cmake_cmd = ['cmake', '--build', '.'] + build_args
        subprocess.check_call(cmake_cmd,
                              cwd=self.build_temp)

        # Copy from build path to some other place from whence it will later be installed.
        # ... or something like that
        # ... setuptools is an enigma monkey patched on a mystery
        if not os.path.exists(dest_path):
            os.makedirs(dest_path, exist_ok=True)
        self.copy_tree(source_path, dest_path)

setuptools.setup(
    name='arbor',
    version=version_,
    python_requires='>=3.6',

    install_requires=['numpy'],
    setup_requires=[],
    zip_safe=False,
    ext_modules=[cmake_extension('arbor')],
    cmdclass={
        'build_ext':   cmake_build,
        'install':     install_command,
        'bdist_wheel': bdist_wheel_command,
    } if WHEEL_INSTALLED else {
        'build_ext':   cmake_build,
        'install':     install_command,
    },

    author='The Arbor dev team.',
    url='https://github.com/arbor-sim/arbor',
    description='High performance simulation of networks of multicompartment neurons.',
    long_description=long_description,
    long_description_content_type='text/markdown',
    classifiers=[
        'Development Status :: 5 - Production/Stable',
        'Intended Audience :: Science/Research',
        'License :: OSI Approved :: BSD License',
        'Programming Language :: Python :: 3.6',
        'Programming Language :: Python :: 3.7',
        'Programming Language :: Python :: 3.8',
        'Programming Language :: Python :: 3.9',
        'Programming Language :: C++',
    ],
    project_urls={
        'Source': 'https://github.com/arbor-sim/arbor',
        'Documentation': 'https://docs.arbor-sim.org',
        'Bug Reports': 'https://github.com/arbor-sim/arbor/issues',
    },
)