#!/bin/sh
# configure script for mac / linux

# script organization:
# section 1: overhead variables
# section 2: tempfiles and heredocs
# section 3: capability detection

# testing before package is ready
if [ -z "${R_HOME:-""}" ]; then
  R_HOME=$(R RHOME)
fi

# initial script vars
# the compiler R thinks is the default
current_cc=$("${R_HOME}/bin/R" CMD config CC)
# the compiler flags R is using by default
# echo "${current_cc}"
current_cflags=$("${R_HOME}/bin/R" CMD config CFLAGS)
# echo "${current_cflags}"
# the preprocessor flags R is using by default
current_cppflags=$("${R_HOME}/bin/R" CMD config CPPFLAGS)
# echo "${current_cppflags}"

# compilation flag notes:
# we respect user specified flags and headers for OpenCL and for CUDA, but not
# for Metal

# openCL notes:
# -l / -framework == tells compiler what to link against
#   '-framework' is generally self-contained, but a homebrew openCL install
#   will need further managing?
# -L == where to look for the shared object
# -I == where to look for the headers
# as far as i can tell, homebrew installations of openCL are missing the 
# ability to find a dot icd file, so they are -as far as i can tell- not a
# valid path forward at this time ...
# most of this is really just practice and preparation for CUDA variable
# handling, and we *shouldn't* really need it here, though providing it for 
# linux users is the right choice

# homebrew opencl carries with it both an
# OpenCL/opencl.h
# AND
# CL/cl.h
# they are not the same, so i'm not sure what the consequences of the difference are ...

# OpenCL variables we should respect if the user has set them:

# ARDEA_OPENCL_INCLUDE
# directory containing CL/cl.h (or OpenCL/opencl.h)
opencl_headers="${ARDEA_OPENCL_INCLUDE:-""}"
if [ -z "${ARDEA_OPENCL_INCLUDE}" ]; then
  opencl_headers=""
  printf "no OpenCL headers specified\n"
  printf "compilation will occur against defaults\n"
else
  if [ -d "${ARDEA_OPENCL_INCLUDE}" ]; then
    opencl_headers="-I${ARDEA_OPENCL_INCLUDE}"
    printf "user supplied headers will be compiled against with:\n"
    printf "%s\n" ${opencl_headers}
  else
    opencl_headers=""
    printf "user supplied %s is not a directory!\n" "${ARDEA_OPENCL_INCLUDE}"
    printf "falling back to default search paths...\n"
  fi
fi

# ARDEA_OPENCL_LIBS
# directory containing libOpenCL.so / libOpenCL.dylib
opencl_shared_objects="${ARDEA_OPENCL_LIBS:-""}"
if [ -z "${ARDEA_OPENCL_LIBS}" ]; then
  opencl_shared_objects=""
  printf "no OpenCL shared objects specified\n"
  printf "compilation will occur against defaults\n"
else
  if [ -d "${ARDEA_OPENCL_LIBS}" ]; then
    opencl_shared_objects="-L${ARDEA_OPENCL_LIBS}"
    # -Wl, == split what follows, up the the next space on commas and pass to
    # ld, i.e. -Wl,-rpath,<some/path/whatever>
    # becomes 'ld -rpath <some/path/whatever>'
    # -rpath bakes a search path for shared library into a binary for runtime
    opencl_direct_pass_flags="-Wl,-rpath,${ARDEA_OPENCL_LIBS}"
    printf "user supplied shared objects will be compiled against with:\n"
    printf "%s\n" ${opencl_shared_objects}
  else
    opencl_shared_objects=""
    opencl_direct_pass_flags=""
    printf "user supplied %s is not a directory!\n" "${ARDEA_OPENCL_LIBS}"
    printf "falling back to default search paths...\n"
  fi
fi

# CUDA variables we *require* for compilation and capability selection
# inference from CUDA_HOME or nvcc's location in the PATH is possible, but
# this script is already complicated ... for now, we're stringent here
# to save ourselves headaches
detect_cuda_libs="${ARDEA_CUDA_LIBS:-""}"
detect_cuda_stubs="${ARDEA_CUDA_STUBS:-""}"
detect_cuda_include="${ARDEA_CUDA_INCLUDE:-""}"
# some CUDA versions may require older compiler versions
alt_compiler="${ARDEA_ALT_COMPILER:-""}"

op_sys="$(uname -s)"

# build our tempdir and trap it
dir01=$(mktemp -d)
trap 'rm -rf "${dir01}" && echo "tempfiles removed\n"' EXIT

opencl_test="${dir01}/opencl_test.c"
opencl_exec="${dir01}/opencl_test"
metal_test="${dir01}/metal_test.m"
metal_exec="${dir01}/metal_test"
cuda_test="${dir01}/cuda_test.cu"
cuda_exec="${dir01}/cuda_test"
# minimal .c (not .cu) test -- we need to test the flags for both
# nvcc and our target compiler
cc_test="${dir01}/cuda_cc_test.c"
cc_exec="${dir01}/cuda_cc_test"

# we use several heredocs to capture our testing scripts
# << capture_this will perform shell expansion
# << 'capture_this' will not perform shell expansion
# preprocessor directives just need the '#' character to be the first non-whitespace character
cat > "${opencl_test}" << 'capture_this'
/* ============================================================================
 * test for openCL platforms and devices
 * return 0 if 1 or more devices are found
 * return 1 if no devices are found
 * 
 * header guard reflects a dichotomy between Apple's deprecated, but
 * still existent openCL framework, and Khronos' standard implementation
 * of OpenCL
 * ========================================================================= */

#ifdef __APPLE__
  #include <OpenCL/opencl.h>
#else
  #include <CL/cl.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* ----------------------------------------------------------------------------
 * wrap clGetDeviceInfo with explicit error checking,
 * so a failed query is visible instead of silently leaving a
 * zero-initialized value that looks like a legitimate "0". 
 * ------------------------------------------------------------------------- */
static int query_device_info(cl_device_id device,
                              cl_device_info param,
                              size_t param_size,
                              void *out,
                              const char *param_label) {
    cl_int err = clGetDeviceInfo(device,
                                 param,
                                 param_size,
                                 out,
                                 NULL);
    if (err != CL_SUCCESS) {
        fprintf(stderr, "  WARNING: failed to query %s (CL error %d)\n",
                param_label, err);
        return 1;
    }
    return 0;
}

/* ----------------------------------------------------------------------------
 * main function
 * return an int as an exit code
 * take in nothing (void)
 * print out device information
 * ------------------------------------------------------------------------- */
int main(void) {
  /* --------------------------------------------------------------------------
   * track the lowest CL_DEVICE_VERSION seen across every device on
   * every platform, so configure can target the weakest device found
   * rather than the strongest (see the ARDEA_OPENCL_MIN_VERSION line
   * emitted at the end of this program). -1 means "no device with a
   * parseable version has been seen yet". 
   * ----------------------------------------------------------------------- */
  int min_cl_major = -1;
  int min_cl_minor = -1;

  cl_uint num_platforms = 0;
  if (clGetPlatformIDs(0, NULL, &num_platforms) != CL_SUCCESS ||
      num_platforms == 0) {
    printf("  No OpenCL platforms found\n");
    return 1;
  }
  
  cl_platform_id *platforms = calloc(num_platforms, sizeof(cl_platform_id));
  if (platforms == NULL) {
    fprintf(stderr, "  ERROR: allocation failure\n");
    return 1;
  }
  clGetPlatformIDs(num_platforms, platforms, NULL);
  
  for (cl_uint p = 0; p < num_platforms; p++) {
    cl_uint num_devices = 0;
    clGetDeviceIDs(platforms[p], CL_DEVICE_TYPE_ALL, 0, NULL, &num_devices);
    if (num_devices == 0) {
      printf("  No devices on this platform\n\n");
      continue;
    }
    
    cl_device_id *devices = calloc(num_devices, sizeof(cl_device_id));
    if (devices == NULL) {
      fprintf(stderr, "  ERROR: allocation failure\n");
      free(platforms);
      return 1;
    }
    clGetDeviceIDs(platforms[p],
                   CL_DEVICE_TYPE_ALL,
                   num_devices,
                   devices,
                   NULL);
    
    for (cl_uint d = 0; d < num_devices; d++) {
      /* -- instantiate our params ----------------------------------------- */
      char name[256] = {0};
      char version[128] = {0};
      char driver_version[128] = {0};
      char device_vendor[256] = {0};
      cl_uint units = 0;
      cl_device_type dtype = 0;
      cl_uint max_dims = 0;
      size_t max_work_group_size = 0;
      cl_ulong global_mem = 0;
      cl_ulong max_alloc = 0;
      cl_ulong local_mem = 0;
      cl_device_local_mem_type local_mem_type = 0;
      
      /* -- query our params ----------------------------------------------- */ 
      query_device_info(devices[d],
                        CL_DEVICE_NAME,
                        sizeof(name),
                        name,
                        "CL_DEVICE_NAME");
      int version_query_err = query_device_info(devices[d],
                        CL_DEVICE_VERSION,
                        sizeof(version),
                        version,
                        "CL_DEVICE_VERSION");
      if (version_query_err == 0) {
        /* -- CL_DEVICE_VERSION is spec-mandated to read
         * "OpenCL <major>.<minor> <platform-specific info>", so this
         * is parsing a guaranteed format, not screen-scraping a
         * vendor-arbitrary string. sscanf's return value is checked
         * so a malformed/unexpected string is skipped (and reported)
         * rather than silently treated as version 0.0. */
        int device_cl_major = 0;
        int device_cl_minor = 0;
        if (sscanf(version, "OpenCL %d.%d", &device_cl_major, &device_cl_minor) == 2) {
          if (min_cl_major < 0 ||
              device_cl_major < min_cl_major ||
              (device_cl_major == min_cl_major && device_cl_minor < min_cl_minor)) {
            min_cl_major = device_cl_major;
            min_cl_minor = device_cl_minor;
          }
        } else {
          fprintf(stderr,
                  "  WARNING: could not parse CL_DEVICE_VERSION string '%s'\n",
                  version);
        }
      }
      query_device_info(devices[d],
                        CL_DEVICE_VENDOR,
                        sizeof(device_vendor),
                        device_vendor,
                        "CL_DEVICE_VENDOR");
      query_device_info(devices[d],
                        CL_DRIVER_VERSION,
                        sizeof(driver_version),
                        driver_version,
                        "CL_DRIVER_VERSION");
      query_device_info(devices[d],
                        CL_DEVICE_TYPE,
                        sizeof(dtype),
                        &dtype,
                        "CL_DEVICE_TYPE");
      query_device_info(devices[d],
                        CL_DEVICE_MAX_COMPUTE_UNITS,
                        sizeof(units),
                        &units,
                        "CL_DEVICE_MAX_COMPUTE_UNITS");
      query_device_info(devices[d],
                        CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS,
                        sizeof(max_dims),
                        &max_dims,
                        "CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS");
      query_device_info(devices[d],
                        CL_DEVICE_MAX_WORK_GROUP_SIZE,
                        sizeof(max_work_group_size),
                        &max_work_group_size,
                        "CL_DEVICE_MAX_WORK_GROUP_SIZE");
      query_device_info(devices[d],
                        CL_DEVICE_GLOBAL_MEM_SIZE,
                        sizeof(global_mem),
                        &global_mem,
                        "CL_DEVICE_GLOBAL_MEM_SIZE");
      query_device_info(devices[d],
                        CL_DEVICE_MAX_MEM_ALLOC_SIZE,
                        sizeof(max_alloc),
                        &max_alloc,
                        "CL_DEVICE_MAX_MEM_ALLOC_SIZE");
      query_device_info(devices[d],
                        CL_DEVICE_LOCAL_MEM_SIZE,
                        sizeof(local_mem),
                        &local_mem,
                        "CL_DEVICE_LOCAL_MEM_SIZE");
      query_device_info(devices[d],
                        CL_DEVICE_LOCAL_MEM_TYPE,
                        sizeof(local_mem_type),
                        &local_mem_type,
                        "CL_DEVICE_LOCAL_MEM_TYPE");
      
      /* ----------------------------------------------------------------------
       * CL_DEVICE_MAX_WORK_ITEM_SIZES is an array sized by max_dims,
       * so it has to be allocated dynamically rather than assumed
       * to be length 3 -- some accelerators report fewer or more. 
       * ------------------------------------------------------------------- */
      size_t *max_item_sizes = NULL;
      if (max_dims > 0) {
        max_item_sizes = calloc(max_dims, sizeof(size_t));
        if (max_item_sizes != NULL) {
          query_device_info(devices[d],
                            CL_DEVICE_MAX_WORK_ITEM_SIZES,
                            max_dims * sizeof(size_t),
                            max_item_sizes,
                            "CL_DEVICE_MAX_WORK_ITEM_SIZES");
        }
      }
      
      printf("  OpenCL device: %s\n", name);
      printf("    type:                  %s\n",
             (dtype & CL_DEVICE_TYPE_GPU) ? "GPU" :
               (dtype & CL_DEVICE_TYPE_CPU) ? "CPU" : "OTHER");
      printf("    device vendor:         %s\n", device_vendor);
      printf("    device version:        %s\n", version);
      printf("    driver version:        %s\n", driver_version);
      printf("    compute units:         %u\n", units);
      printf("    max work-group size:   %zu\n", max_work_group_size);
      printf("    max work-item dims:    %u\n", max_dims);
      if (max_item_sizes != NULL) {
        printf("    max work-item sizes:   ");
        for (cl_uint i = 0; i < max_dims; i++) {
          printf("%zu ", max_item_sizes[i]);
        }
        printf("\n");
        free(max_item_sizes);
      }
      printf("    global mem (bytes):    %llu\n", (unsigned long long)global_mem);
      printf("    max single alloc:      %llu\n", (unsigned long long)max_alloc);
      printf("    local mem (bytes):     %llu\n", (unsigned long long)local_mem);
      printf("    local mem type:        %s\n",
             local_mem_type == CL_LOCAL ? "dedicated (CL_LOCAL)" :
             local_mem_type == CL_GLOBAL ? "emulated (CL_GLOBAL)" : "unknown");
      printf("\n");
    }
    free(devices);
  }
  free(platforms);

  /* -- emit a single, fixed-format line the calling shell script can
   * grep out unambiguously, rather than parsing the human-readable
   * "device version:" lines printed above (which are free to change
   * their spacing/wording without that being a compatibility break).
   * encoding follows the Khronos convention of major*100 + minor*10,
   * e.g. 1.2 -> 120, 2.1 -> 210, 3.0 -> 300. printed as UNKNOWN when
   * no device offered a parseable CL_DEVICE_VERSION, so the caller can
   * tell "detected nothing" apart from "detected 0.0". */
  if (min_cl_major >= 0) {
    printf("ARDEA_OPENCL_MIN_VERSION=%d\n", (min_cl_major * 100) + (min_cl_minor * 10));
  } else {
    printf("ARDEA_OPENCL_MIN_VERSION=UNKNOWN\n");
  }

  return 0;
}

capture_this

cat > "${metal_test}" << 'capture_this'
/* ============================================================================
 * test for metal Framework on apple devices
 * 
 * obj-c implementation that can just be called immediately once it has been
 * compiled, prints out some information about the device
 *
 * currently no fancy footwork required, because this should only be called
 * on an Apple OS, where the situation should be pretty boring
 * ========================================================================= */

#import <Foundation/Foundation.h>
#import <Metal/Metal.h>

int main() {
  // the @autoreleasepool is a tool for simplifying cleanup, if everything i
  // create within the function needs to be released when the function ends,
  // I just start up the @autoreleasepool right away, if things need to persist
  // then they can't be created within the pool
  @autoreleasepool {
    NSArray<id<MTLDevice>> *devices = MTLCopyAllDevices();
      
      // its important that this happens inside the autoreleasepool, though
      // it's still a little opaque to me as to why
      if (devices.count == 0) {
        printf("No Metal devices found\n");
        return 1;
      }
        
      printf("Found %lu Metal device(s)\n\n", (unsigned long)devices.count);
        
      for (int i = 0; i < devices.count; i++) {
          id<MTLDevice> device = devices[i];
          printf("Device %d: %s\n", i, [device.name UTF8String]);
          printf("  Location: %s\n", device.location == MTLDeviceLocationBuiltIn ? "Built-in" : "External");
          printf("  Low Power: %s\n", device.lowPower ? "Yes" : "No");
          printf("  Headless: %s\n", device.headless ? "Yes" : "No");
          printf("  Removable: %s\n", device.removable ? "Yes" : "No");
          printf("  Max Threads Per Threadgroup: %lu\n", (unsigned long)device.maxThreadsPerThreadgroup.width);
          printf("  Recommended Max Working Set Size: %.2f MB\n", 
                 device.recommendedMaxWorkingSetSize / (1024.0 * 1024.0));
          printf("  Threadgroup Width: %lu\n", [device maxThreadsPerThreadgroup].width);
          printf("  Threadgroup Height: %lu\n", [device maxThreadsPerThreadgroup].height);
          printf("  Threadgroup Depth: %lu\n", [device maxThreadsPerThreadgroup].depth);
          printf("\n");
      }
  }
  return 0;
}

capture_this

cat > "${cuda_test}" << 'capture_this'
/* ============================================================================
 * test for NVIDIA CUDA devices
 * return 0 if 1 or more devices are found
 * return 1 if no devices are found
 *
 * requires the CUDA toolkit to be installed, specifically nvcc and the
 * cuda_runtime.h header - typically found under /usr/local/cuda on Linux
 * ========================================================================= */

#include <cuda_runtime.h>
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int num_devices = 0;
    cudaError_t err = cudaGetDeviceCount(&num_devices);

    if (err != cudaSuccess || num_devices == 0) {
        printf("No CUDA devices found\n");
        return 1;
    }

    printf("Found %d CUDA device(s)\n\n", num_devices);

    for (int d = 0; d < num_devices; d++) {
        struct cudaDeviceProp props;
        cudaGetDeviceProperties(&props, d);
        printf("Device %d: %s\n", d, props.name);
        printf("  Compute Capability: %d.%d\n", props.major, props.minor);
        printf("  Multiprocessors: %d\n", props.multiProcessorCount);
        printf("  Global Memory: %.2f MB\n", props.totalGlobalMem / (1024.0 * 1024.0));
        printf("  Max Threads Per Block: %d\n", props.maxThreadsPerBlock);
        printf("  Warp Size: %d\n", props.warpSize);
        printf("\n");
    }
    return 0;
}

capture_this

cat > "${cc_test}" << 'capture_this'
#include <cuda_runtime.h>
#include <cuda.h>
int main(void) {
  int n = 0;
  cudaGetDeviceCount(&n);
  return 0;
}

capture_this

echo "checking OS ..."
# setting OS specific flags for tests ...
# case matches aren't analogous to test / [ ... ], check docs before adding any
# pattern matching...
case "${op_sys}" in
  Darwin)
    # mac config stuff here
    echo "  detected Darwin, setting OS specific flags:"
    # when invoking this variable later
    # it stays unquoted because of the space
    opencl_link_flags="-framework OpenCL"
    echo "    '${opencl_link_flags}'"
    ;;
  Linux)
    # linux config stuff here
    echo "  detected a Linux distribution, setting OS specific flags:"
    opencl_link_flags="-lOpenCL"
    echo "    '${opencl_link_flags}'"
    ;;
  *)
    echo "  unsupported/untested OS detected:" >&2
    echo "    '${op_sys}'"
    echo "    please contact maintainer"
    exit 1
    ;;
esac
echo " "

echo "checking for OpenCL framework"
# test openCL capabilities
# calls to '-l' need to occur AFTER calls to '-L'
# order here:
# '-I' then '-L' then '-l'

arg_string=""
for compilation_arg in ${opencl_headers} \
  ${opencl_shared_objects} \
  ${opencl_link_flags} \
  ${opencl_direct_pass_flags};
do
  if [ -n "${compilation_arg}" ]; then
    # echo "detected arg = ${compilation_arg}"
    arg_string="${arg_string}${compilation_arg} "
    # echo "new string = ${arg_string}"
  fi
done

# build as a string with spaces
compilation_call="${current_cc} ${arg_string}${opencl_test} -o ${opencl_exec}"
printf "testing OpenCL compilation via tempfiles with:\n  %s\n" "${compilation_call}"

# execute without quotes so the shell evaluates spaces correctly...
# drop error redirect just in case ...
${compilation_call} 2>/dev/null
compilation_result=$?

# quoting my current compiler invocation worked at one point, but broke after
# changing some environment variables, so i need to double check what the
# built in defaults are ...
# ${current_cc} "${opencl_test}" \
#   ${opencl_headers} \
#   ${opencl_shared_objects} \
#   ${opencl_link_flags} \
#   ${opencl_direct_pass_flags} \
#   -o "${opencl_exec}" 2>/dev/null
# compilation_result=$?

# hypothetically this just needs to compile without complaint to greenlight
# installation

# if the exit condition of compilation is zero, then compilation was a success
# detected_cl_target stays empty unless the probe below reports a valid,
# all-digit minimum device version; this is the value CL_TARGET_OPENCL_VERSION
# gets set to, so it starts empty (rather than a hardcoded guess) and is
# only populated by a validated probe output further down.
detected_cl_target=""
opencl_exists=1
opencl_devices_exist=1

if [ ${compilation_result} -eq 0 ]; then
  echo "OpenCL test script compiled without complaint."
  # capture stdout so we can both show it to the user (as before) and grep
  # the machine-readable ARDEA_OPENCL_MIN_VERSION line out of it; $? right
  # after a command-substitution assignment still reflects that command's
  # exit status, so test_result below is unaffected by the capture.
  opencl_probe_output=$("${opencl_exec}")
  test_result=$?
  printf '%s\n' "${opencl_probe_output}"
  if [ ${test_result} -eq 0 ]; then
    echo "OpenCL devices discovered."
    opencl_exists=0
    opencl_devices_exist=0
  else
    echo "OpenCL devices appear absent."
    opencl_exists=0
    opencl_devices_exist=1
    # is this machine a build only machine?
  fi

  # pull the sentinel line out of the probe's output, if present
  detected_cl_target=$(printf '%s\n' "${opencl_probe_output}" | \
    grep '^ARDEA_OPENCL_MIN_VERSION=' | \
    sed 's/^ARDEA_OPENCL_MIN_VERSION=//')

  # defensive: only trust a strictly-numeric result (this also rejects
  # "UNKNOWN", a missing line, and anything else unexpected) before it
  # ever gets anywhere near a compiler command line
  case "${detected_cl_target}" in
    ''|*[!0-9]*)
      detected_cl_target=""
      ;;
  esac

  if [ -n "${detected_cl_target}" ]; then
    printf "detected OpenCL device(s) report a minimum supported version corresponding to CL_TARGET_OPENCL_VERSION=%s\n" "${detected_cl_target}"
  else
    printf "could not determine a device-reported OpenCL version; CL_TARGET_OPENCL_VERSION will fall back to the source-level default (120)\n"
  fi
else
  echo "OpenCL test script failed to compile. Please check for OpenCL framework."
  echo "  WARNING: user supplied directories that contain spaces are not compatible with this configuration setup."
  opencl_exists=1
  opencl_devices_exist=1
fi
echo " "

opencl_target_define=""
if [ -n "${detected_cl_target}" ]; then
  opencl_target_define="-DCL_TARGET_OPENCL_VERSION=${detected_cl_target}"
fi

if [ ${opencl_exists} -eq 0 ]; then
  DEFINE_HAVE_OPENCL="#define HAVE_OPENCL 1"
  OPENCL_OBJECTS='$(OPENCL_C:.c=.o)'
  OPENCL_CPPFLAGS="${opencl_headers} ${opencl_target_define}"
  # collapse repeated spaces (e.g. when opencl_headers is empty) and trim
  # leading/trailing whitespace so an empty detection doesn't leave a stray
  # space at the front/back of the flag string
  OPENCL_CPPFLAGS=$(printf '%s' "${OPENCL_CPPFLAGS}" | tr -s ' ')
  OPENCL_CPPFLAGS=$(printf '%s' "${OPENCL_CPPFLAGS}" | sed -e 's/^ *//' -e 's/ *$//')
  OPENCL_LIBS="${opencl_shared_objects} ${opencl_link_flags} ${opencl_direct_pass_flags}"
  OPENCL_LIBS=$(printf '%s' "${OPENCL_LIBS}" | tr -s ' ')
  printf "OpenCL 'CPPFLAGS' inferred as:\n  %s\n" "${OPENCL_CPPFLAGS}"
  printf "OpenCL associated PKG_LIBS inferred as:\n %s\n" "${OPENCL_LIBS}"
else
  DEFINE_HAVE_OPENCL="/* HAVE_OPENCL not defined: OpenCL unavailable on this build */"
  OPENCL_OBJECTS=""
  OPENCL_CPPFLAGS=""
  OPENCL_LIBS=""
fi

# end openCL testing
# begin Metal testing
# nothing here should exit 1, because metal capabilities need to be optional,
# but we need to communicate clearly which files we're adding to the Makevars
# and where sentinel functions are calling underlying compiled functions,
# or strategically no-op-ing 

metal_exists=1
metal_devices_exist=1

# we only test Darwin and Linux here because if we have anything else we 
# will have already bailed out
case "${op_sys}" in
  Darwin)
    # test for metal here
    echo "Probing Metal capabilities"
    echo " "
    ${current_cc} \
      "${metal_test}" \
      -fobjc-arc \
      -framework Metal \
      -framework Foundation \
      -o "${metal_exec}" 2>/dev/null
      compilation_result=$?
      
      if [ ${compilation_result} -eq 0 ]; then
        echo "Metal test compiled without complaint."
        "${metal_exec}"
        test_result=$?
        if [ "${test_result}" -eq 0 ]; then
          metal_exists=0
          metal_devices_exist=0
          echo "Metal devices discovered."
          echo " "
          # make edits to makevars.in to compile metal tooling
        else
          metal_exists=0
          metal_devices_exist=1
          echo "Metal devices appear absent."
          echo " "
        fi
      else
        metal_exists=1
        metal_devices_exist=1
        echo "Metal test did not compile"
        echo " "
      fi
    ;;
  Linux)
    # no need to test for metal on non-darwin OS
    metal_exists=1
    metal_devices_exist=1
    echo "Skipping tests for Metal"
    echo " "
    ;;
esac

if [ ${metal_exists} -eq 0 ]; then
  METAL_OBJECTS='$(METAL_C:.c=.o) $(METAL_M:.m=_m.o)'
  METAL_CPPFLAGS=""
  METAL_LIBS="-framework Metal -framework Foundation"
  DEFINE_HAVE_METAL="#define HAVE_METAL 1"
  printf "Metal 'CPPFLAGS' inferred as:\n  %s\n" "${METAL_CPPFLAGS}"
  printf "Metal associated PKG_LIBS inferred as:\n %s\n" "${METAL_LIBS}"
  echo " "
else
  METAL_OBJECTS=""
  METAL_CPPFLAGS=""
  METAL_LIBS=""
  DEFINE_HAVE_METAL="/* HAVE_METAL not defined: Metal unavailable on this build */"
fi

# set missing capabilities as the default
cuda_exists=1
cuda_devices_exist=1

if [ -d "${detect_cuda_libs}" ] && [ -d "${detect_cuda_stubs}" ] && [ -d "${detect_cuda_include}" ]; then
  # for now, we can only set cuda_exists to zero inside here
  if [ -d "${alt_compiler}" ]; then
    printf "an alternative compiler for CUDA has been specified:\n"
    printf "  %s\n" ${alt_compiler}
    cuda_cc=${alt_compiler}
  else
    cuda_cc=${current_cc}
  fi
  command -v nvcc > /dev/null 2>&1
  nvcc_present=$?
  if [ ${nvcc_present} -eq 0 ]; then
    nvcc "${cuda_test}" \
      ${alt_compiler:+--compiler-bindir "${alt_compiler}"} \
      -I"${detect_cuda_include}" \
      -L"${detect_cuda_libs}" \
      -o "${cuda_exec}" 2>"${dir01}/cuda_compile_err"
    compilation_result=$?
    
    "${current_cc}" ${current_cflags} ${current_cppflags} \
      -I"${detect_cuda_include}" \
      "${cc_test}" -o "${cc_exec}" \
      -L"${detect_cuda_libs}" -lcudart \
      -L"${detect_cuda_stubs}" -lcuda \
      2>"${dir01}/cuda_cc_err"
    cuda_cc_result=$?
      
    if [ ${cuda_cc_result} -eq 0 ] && [ ${compilation_result} -eq 0 ]; then
      cuda_exists=0
      "${cuda_exec}"
      test_result=$?
      if [ ${test_result} -ne 0 ]; then
        cuda_exists=0
        printf "CUDA compilations were successful, but no devices were discovered\n"
        printf "CUDA capabilities will be compiled, but devices will be required\n"
        printf "for actual code dispatch\n"
      else
        cuda_devices_exist=0
      fi
    else
      cuda_exists=1
      printf "CUDA compilation tests did not succeed, the following must be true:\n"
      printf "  nvcc must be in R's PATH\n"
      printf "  ARDEA_CUDA_LIBS/STUBS/INCLUDE must be pointed to working CUDA dirs\n"
    fi
      
  else
    printf "nvcc was not detected, ending CUDA tests\n"
    printf "nvcc must be in R's PATH to pass build checks\n"
  fi
fi



if [ ${cuda_exists} -eq 0 ]; then
  CUDA_OBJECTS='$(CUDA_C:.c=.o)'
  CUDA_CPPFLAGS="-I${detect_cuda_include}"
  CUDA_LIBS="-L${detect_cuda_libs} -lcudart -L${detect_cuda_stubs} -lcuda -Wl,-rpath,${detect_cuda_libs}"
  DEFINE_HAVE_CUDA="#define HAVE_CUDA 1"
  printf "CUDA 'CPPFLAGS' inferred as:\n  %s\n" "${CUDA_CPPFLAGS}"
  printf "CUDA associated PKG_LIBS inferred as:\n %s\n" "${CUDA_LIBS}"
  echo " "
else
  CUDA_OBJECTS=""
  CUDA_CPPFLAGS=""
  CUDA_LIBS=""
  DEFINE_HAVE_CUDA="/* HAVE_CUDA not defined: CUDA unavailable on this build */"
  printf "CUDA capabilities not detected\n"
fi

sed -e "s|@OPENCL_CPPFLAGS@|${OPENCL_CPPFLAGS}|" \
  -e "s|@OPENCL_LIBS@|${OPENCL_LIBS}|" \
  -e "s|@OBJECTS_METAL@|${METAL_OBJECTS}|" \
  -e "s|@METAL_CPPFLAGS@|${METAL_CPPFLAGS}|" \
  -e "s|@METAL_LIBS@|${METAL_LIBS}|" \
  -e "s|@OBJECTS_CUDA@|${CUDA_OBJECTS}|" \
  -e "s|@CUDA_CPPFLAGS@|${CUDA_CPPFLAGS}|" \
  -e "s|@CUDA_LIBS@|${CUDA_LIBS}|" \
  -e "s|@OBJECTS_OPENCL@|${OPENCL_OBJECTS}|" \
  src/Makevars.in > src/Makevars

config_placeholder="${dir01}/config.h.in"

cat > "${config_placeholder}" << 'capture_this'
/* src/config.h.in
 * Template for a generated header describing which optional GPU backends
 * were detected at configure time.
 */

#ifndef ARDEA_CONFIG_H
#define ARDEA_CONFIG_H

/* ----------------------------------------------------------------------------
 * We explicitly define when compilation is successful for a framework
 * this is distinct from devices compliant to that framework being present
 * ------------------------------------------------------------------------- */
@DEFINE_HAVE_METAL@

@DEFINE_HAVE_CUDA@

@DEFINE_HAVE_OPENCL@

#endif /* ARDEA_CONFIG_H */
capture_this

sed -e "s|@DEFINE_HAVE_METAL@|${DEFINE_HAVE_METAL}|" \
  -e "s|@DEFINE_HAVE_CUDA@|${DEFINE_HAVE_CUDA}|" \
  -e "s|@DEFINE_HAVE_OPENCL@|${DEFINE_HAVE_OPENCL}|" \
  "${config_placeholder}" > src/config.h

# R/sysdata.rda is reserved data file that is loaded into the
# package's internal namespace and is not exported
"${R_HOME}/bin/Rscript" -e "
ardea_vars <- list(cuda_include = '${detect_cuda_include}',
                   cuda_libs = '${detect_cuda_libs}',
                   cuda_stubs = '${detect_cuda_stubs}',
                   opencl_libs = '${ARDEA_OPENCL_LIBS}',
                   opencl_include = '${ARDEA_OPENCL_INCLUDE}',
                   opencl_min_target = '${detected_cl_target}',
                   opencl_success = as.integer('${opencl_exists}'),
                   opencl_devices_exist = as.integer('${opencl_devices_exist}'),
                   metal_success = as.integer('${metal_exists}'),
                   metal_devices_exist = as.integer('${metal_devices_exist}'),
                   cuda_success = as.integer('${cuda_exists}'),
                   cuda_devices_exist = as.integer('${cuda_devices_exist}'))
save(ardea_vars, file = 'R/sysdata.rda')
"

