"""Fetch and build the pinned public SelfForce-1D without installing globally.

The compiler, GSL and BLAS/LAPACK must already be available. On macOS the
existing Accelerate framework supplies BLAS/LAPACK. Only two upstream build
configuration files are changed; numerical source files are verified against
the pinned Git revision before and after configuration.
"""
import argparse
from importlib.metadata import version
import json
import os
from pathlib import Path
import platform
import shutil
import subprocess
import sys
import tarfile

REVISION = '8390ca611ebcb2b644df6f59c15b1bafedf4c9f2'
REPOSITORY = 'https://bitbucket.org/peterdiener/selfforce-1d.git'

def output(command, cwd=None):
    return subprocess.check_output(command, cwd=cwd, text=True).strip()

def check_sources(source):
    revision = output(['git','rev-parse','HEAD'], source)
    if revision != REVISION:
        raise RuntimeError('Wrong upstream revision: ' + revision)
    names = output(['git','diff','--name-only','HEAD'], source).splitlines()
    if set(names)-{'SConstruct','Src/SConscript'}:
        raise RuntimeError('Numerical sources differ from pinned upstream: ' + repr(names))

def build(args):
    work = args.work.resolve()
    work.mkdir(parents=True, exist_ok=True)
    source = work/'SelfForce-1D'
    if not source.exists():
        # Fetch the immutable revision, not a branch whose tip can move.
        subprocess.run(['git','init',str(source)], check=True)
        subprocess.run(['git','remote','add','origin',REPOSITORY],
                       cwd=source, check=True)
        subprocess.run(['git','fetch','--depth','1','origin',REVISION],
                       cwd=source, check=True)
        subprocess.run(['git','checkout','--detach','FETCH_HEAD'],
                       cwd=source, check=True)
    check_sources(source)
    gsl = args.gsl_prefix.resolve()
    if not (gsl/'include/gsl/gsl_version.h').is_file():
        raise RuntimeError('GSL header missing from --gsl-prefix')
    for compiler in (args.fc,args.cxx):
        if not shutil.which(compiler):
            raise RuntimeError('Compiler not found: ' + compiler)
    flags = ['-O3','-g','-fopenmp']
    linkflags = ['-g','-fopenmp']
    libraries = ['stdc++','gsl','m','gslcblas']
    if platform.system() == 'Darwin':
        sdk=output(['xcrun','--show-sdk-path'])
        systemflags=['-mmacosx-version-min=13.0','-isysroot',sdk]
        flags += systemflags
        linkflags += systemflags+['-framework','Accelerate']
    elif platform.system() == 'Linux':
        libraries += ['lapack','blas']
    else:
        raise RuntimeError('Build helper supports macOS/Linux; use upstream instructions elsewhere')
    config = '''import os
env = DefaultEnvironment(ENV={{'PATH':os.environ['PATH']}}, LINK={fc!r},
  LINKFLAGS={linkflags!r}, TOOLS=['default','gfortran'])
env['LIBS']={libraries!r}
env['LIBPATH']=[{libpath!r}]
env['F90FILESUFFIXES']=['.f90','.f']
env['F90']={fc!r}
env['FORTRAN']={fc!r}
env['F90FLAGS']={f90flags!r}
env['FORTRANFLAGS']={flags!r}
env['FORTRANMODDIR']='${{TARGET.dir}}'
env['CPPPATH']={includes!r}
env['CXXFLAGS']={cxxflags!r}
env['CXX']={cxx!r}
Export('env')
SConscript('Src/SConscript', exports='env', variant_dir='Build', duplicate=0)
Decider('MD5-timestamp')
'''.format(fc=args.fc,cxx=args.cxx,linkflags=linkflags,libraries=libraries,
           libpath=str(gsl/'lib'), flags=flags,
           f90flags=flags+['-ffree-line-length-none','-Wno-missing-include-dirs'],
           includes=[str(gsl/'include'),'EffSource/ScalarSchwarzschild/EffectiveSource',
                     'EffSource/RWZSchwarzschild/EffectiveSource'],
           cxxflags=flags+['-std=c++11','-Wall','-Wno-unknown-pragmas'])
    (source/'SConstruct').write_text(config)
    script=output(['git','show','HEAD:Src/SConscript'],source)+'\n'
    for kind in ('Scalar','RWZ'):
        line="SConscript('EffSource/"+kind+"Schwarzschild/EffectiveSource/SConscript')"
        if script.count(line)!=1:
            raise RuntimeError('Unexpected source build layout')
        script=script.replace(line,'# Reuse the pinned, committed generated Wigner-D headers.')
    (source/'Src/SConscript').write_text(script)
    check_sources(source)
    scons=work/'scons-local'
    if not (scons/'scons.py').exists():
        scons.mkdir(exist_ok=True)
        with tarfile.open(source/'SCons-local/scons-local-3.1.1.tar.gz') as archive:
            for member in archive.getmembers():
                target=(scons/member.name).resolve()
                if scons.resolve() not in target.parents and target!=scons.resolve():
                    raise RuntimeError('Unsafe archive path')
                if member.issym() or member.islnk():
                    raise RuntimeError('Unexpected archive link')
            archive.extractall(scons)
    environment=dict(revision=REVISION,repository=REPOSITORY,
                     python=sys.version,platform=platform.platform(),
                     numpy_version=version('numpy'),
                     fortran=output([args.fc,'--version']).splitlines()[0],
                     cxx=output([args.cxx,'--version']).splitlines()[0],
                     gsl_version=output([str(gsl/'bin/gsl-config'),'--version']),
                     build_flags=flags,link_flags=linkflags,libraries=libraries)
    (work/'environment.json').write_text(json.dumps(environment,indent=2)+'\n')
    # Upstream's first-build Fortran module dependencies require serial SCons.
    subprocess.run([sys.executable,str(scons/'scons.py'),'Build/Exe/test.x'],
                   cwd=source,check=True)
    check_sources(source)
    if args.probe:
        subdir='EffSource/ScalarSchwarzschild/EffectiveSource'
        probe=work/'source-probe'
        command=[args.cxx,*flags,'-std=c++11',
                 '-I'+str(source/'Src'/subdir),'-I'+str(gsl/'include'),
                 str(Path(__file__).resolve().parent/'probe_source.cpp'),
                 str(source/'Build'/subdir/'EffectiveSource-acceleration.o'),
                 str(source/'Build'/subdir/'WignerDMatrix.o'),
                 '-L'+str(gsl/'lib'),'-lgsl','-lgslcblas','-o',str(probe)]
        subprocess.run(command,check=True)
        with (work/'source-probes.csv').open('w') as output_file:
            subprocess.run([str(probe)],stdout=output_file,check=True)
    print(source/'Build/Exe/test.x')

if __name__=='__main__':
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--work',type=Path,required=True)
    parser.add_argument('--gsl-prefix',type=Path,required=True)
    parser.add_argument('--fc',default='gfortran')
    parser.add_argument('--cxx',default='g++')
    parser.add_argument('--probe',action='store_true',
                        help='Also compile and execute the independent source probe')
    build(parser.parse_args())
