# syntax=docker/dockerfile:1.4
# Dockerfile for CORSIKA7
#
# Image with different config variants of CORSIKA:
#
# - flat and curved atmosphere
# - high- / and low-energy hadronic interaction models
#
# The variants are build depending on the configuration files provided by CTAO.
#
# CORSIKA can be build with or without optimization flags (AVX2/AVX512).

ARG BASE_IMAGE
ARG BUILD_BASE_IMAGE

# hadolint global ignore=DL3013,DL3033,DL3041,DL4006,SC1091,SC2086
# - DL3013, DL3033: ignore warnings about installing non-specific versions with microdnf or yum)
# - DL4006: ignore pipefail warning (SHELL is set at stage level)
# - SC1091: ignore warning about not being able to source the bashrc
# - SC2086: ignore variable quoting in complex shell expressions

# -----------------------------------------------------------------------------
# STAGE 1: source code preparation
# -----------------------------------------------------------------------------
FROM ${BASE_IMAGE} AS source_preparation
ARG CORSIKA_VERSION
ARG CORSIKA_OPT_PATCH_VERSION
ARG CORSIKA_OPT_PATCH_REVISION
ARG CORSIKA_OPT_PATCH_SOURCE_URL
ARG CORSIKA_CONFIG_VERSION
ARG CORSIKA_CONFIG_REVISION
ARG CORSIKA_CONFIG_SOURCE_URL
ARG AUTOCONF_SHA256

RUN microdnf update -y && microdnf install -y git gzip tar && microdnf clean all
WORKDIR /src

# Clone CORSIKA configuration files from CTAO gitlab
RUN git config --global advice.detachedHead false && \
    git clone --depth 1 --branch "${CORSIKA_CONFIG_VERSION}" "${CORSIKA_CONFIG_SOURCE_URL}" && \
    git -C corsika7-config rev-parse HEAD > /src/corsika_config_revision && \
    if [ -n "${CORSIKA_CONFIG_REVISION}" ]; then \
        test "$(cat /src/corsika_config_revision)" = "${CORSIKA_CONFIG_REVISION}"; \
    fi && \
    cp -v corsika7-config/config/"${CORSIKA_VERSION}"/*.h . && \
    rm -rf corsika7-config

# CORSIKA source is downloaded once per release by the GitHub workflow.
COPY corsika-source/corsika7 /src/corsika7
COPY corsika-source/corsika_revision /src/corsika_revision

# Clone CORSIKA optimization patches from CTAO gitlab
RUN git config --global advice.detachedHead false && \
    git clone --depth 1 --branch "${CORSIKA_OPT_PATCH_VERSION}" "${CORSIKA_OPT_PATCH_SOURCE_URL}" && \
    git -C corsika-opt-patches rev-parse HEAD > /src/corsika_opt_patch_revision && \
    if [ -n "${CORSIKA_OPT_PATCH_REVISION}" ]; then \
        test "$(cat /src/corsika_opt_patch_revision)" = "${CORSIKA_OPT_PATCH_REVISION}"; \
    fi && \
    cp -rL corsika-opt-patches/build_opt/"corsikaOptPatch-${CORSIKA_VERSION}"/* corsika7/ && \
    rm -rf corsika-opt-patches

# autoconf archive (tar file needs to be present in the build context)
COPY autoconf.tar.gz /tmp/autoconf.tar.gz
# hadolint ignore=DL4006
RUN sha256sum /tmp/autoconf.tar.gz | cut -d ' ' -f1 > /src/autoconf_sha256 && \
    if [ -n "${AUTOCONF_SHA256}" ]; then \
        test "$(cat /src/autoconf_sha256)" = "${AUTOCONF_SHA256}"; \
    fi && \
    tar -xzf /tmp/autoconf.tar.gz -C /src

# -----------------------------------------------------------------------------
# STAGE 2: build_image
# -----------------------------------------------------------------------------
FROM ${BUILD_BASE_IMAGE} AS build_image
ARG AVX_FLAG
ARG CORSIKA_VERSION
ARG CORSIKA_OPT_PATCH_VERSION
ARG CORSIKA_OPT_PATCH_SOURCE_URL
ARG CORSIKA_CONFIG_VERSION
ARG CORSIKA_CONFIG_SOURCE_URL
ARG CORSIKA_SOURCE_URL
ARG CORSIKA_CONFIG_REVISION
ARG CORSIKA_OPT_PATCH_REVISION
ARG AUTOCONF_VERSION
ARG C_SPECIFIC_FLAGS="-std=c99"
ARG FORTRAN_SPECIFIC_FLAGS="-ffixed-line-length-132 -fno-automatic -frecord-marker=4 -std=legacy"

RUN yum update -y --setopt=tsflags=nodocs && \
    yum install -y automake diffutils csh gcc gcc-c++ gcc-fortran libtool m4 patch && \
    yum clean all

WORKDIR /workdir/simulation_software
COPY --from=source_preparation /src .
COPY --from=source_preparation /src/corsika7/bernlohr/iact.c /tmp/iact.c
COPY docker/validate_corsika7_build.sh /usr/local/bin/validate_corsika7_build

# Install autoconf 2.71 - needed to apply CORSIKA patches
WORKDIR /workdir/simulation_software/autoconf-${AUTOCONF_VERSION}
RUN ./configure --prefix=/usr && \
    make && make install

WORKDIR /workdir/simulation_software/corsika7

# Apply patches for non-optimized / optimized CORSIKA
RUN if [ "$CORSIKA_VERSION" = "77550" ]; then \
        patch -b -p0 src/corsika.F < "../corsika-77550.patch"; \
    fi && \
    if [ "$AVX_FLAG" != "generic" ]; then \
        ./corsika_opt_patching.sh && \
        autoreconf; \
    fi

# Build all CORSIKA versions (each config*.h file results in one build)
RUN set -e; ls ../config_*.h; for config_file in ../config_*.h; do \
        ./coconut -d && rm -f ./*/*.a lib/*/*.a include/*.h \
            ./run/corsika"${CORSIKA_VERSION}"Linux_* && \
        # Extract variant and model names from config file name
        variant=$(basename "$config_file" .h | sed 's/.*_\(curved\|flat\)$/\1/'); \
        he_model=$(basename "$config_file" .h | sed 's/^config_\([^_]*\)_.*/\1/'); \
        # CTAO specific CORSIKA configuration
        cp -v "$config_file" include/config.h; \
        # compilation flags (with or without optimization)
        AVX_EXTRA_FLAG=$([ "$AVX_FLAG" = "avx512f" ] && echo "-ffp-contract=off" || echo "") && \
        optimization="generic"; \
        if [ "$AVX_FLAG" = "generic" ] || [ "$variant" = "curved" ]; then \
            BASE_FLAGS="-g -O3"; \
        # Optimization config options
        else \
            optimization="$AVX_FLAG"; \
            AVX_COMPILER_FLAG="-m${AVX_FLAG}"; \
            BASE_FLAGS="-g -DCERENKOPT -DVLIBM -O3 ${AVX_COMPILER_FLAG} ${AVX_EXTRA_FLAG} \
                -DVECTOR_SIZE=8 -DVECTOR_LENGTH=8"; \
            { \
                echo "#define __CERENKOPT__ 1"; \
                echo "#define __VLIBM__ 1"; \
                echo "#define __CACHE_CERENKOPT__"; \
                echo "#define __CACHE_VLIBM__"; \
            } >> include/config.h; \
        fi && \
        export MAKEFLAGS="-j4" \
            CFLAGS="${BASE_FLAGS} ${C_SPECIFIC_FLAGS}" \
            CXXFLAGS="${BASE_FLAGS} ${C_SPECIFIC_FLAGS}" \
            FFLAGS="${BASE_FLAGS} ${FORTRAN_SPECIFIC_FLAGS}" && \
        CORSIKA_USER_COMP=1 ./coconut --expert --without-root --without-COASTUSERLIB --without-conex < /dev/null; \
        # Require exactly one newly built executable before moving it
        set -- ./run/corsika"${CORSIKA_VERSION}"Linux_*; \
        if [ "$#" -ne 1 ] || [ ! -x "$1" ]; then exit 1; fi; \
        rm -v -f ./run/qgsdat-II-04 && \
        CORSIKA_RUN_DIR="/workdir/corsika-run" && \
        mkdir -p "$CORSIKA_RUN_DIR" && \
        corsika_name=$(basename "$config_file" .h | sed 's/^config_/corsika_/'); \
        hilow_default=$(sed -n 's/^[Cc*][[:space:]]*HILOW[[:space:]]*\([0-9][0-9.]*\)[[:space:]]*$/\1/p' src/corsikacompilefile.f) && \
        test -n "$hilow_default" && \
        mv -v "$1" "${CORSIKA_RUN_DIR}/${corsika_name}" && \
        printf '%s\n' "$hilow_default" > "${CORSIKA_RUN_DIR}/.${corsika_name}.hilow_default" && \
        validate_corsika7_build include/config.h "${CORSIKA_RUN_DIR}/${corsika_name}" \
            "$CORSIKA_VERSION" "$he_model" "$variant" "$optimization" "/tmp/${corsika_name}" && \
        rm -rf -- "/tmp/${corsika_name}" && \
        # NUCNUCS table required to be in same directory as executables
        cp -v -f ./run/NUCNUCCS "$CORSIKA_RUN_DIR/"; \
    done

# Generate build_opts.yml with build information
RUN CORSIKA_RUN_DIR="/workdir/corsika-run" && \
    IACT_ATMO_VERSION=$(awk '\
        /#[[:space:]]*define[[:space:]]+IACT_ATMO_VERSION[[:space:]]+"/ {\
            match($0, /"[^"]*"/);\
            if (RSTART) {\
                print substr($0, RSTART + 1, RLENGTH - 2);\
                exit;\
            }\
        }' /tmp/iact.c) && \
    if [ -z "${IACT_ATMO_VERSION}" ]; then \
        IACT_ATMO_VERSION="unknown"; \
    fi && \
    { \
        echo "corsika_version: \"${CORSIKA_VERSION}\""; \
        echo "corsika_revision: \"$(cat /workdir/simulation_software/corsika_revision)\""; \
        echo "corsika_source_url: \"${CORSIKA_SOURCE_URL}\""; \
        echo "corsika_opt_patch_version: \"${CORSIKA_OPT_PATCH_VERSION}\""; \
        echo "corsika_opt_patch_revision: \"$(cat /workdir/simulation_software/corsika_opt_patch_revision)\""; \
        echo "corsika_opt_patch_source_url: \"${CORSIKA_OPT_PATCH_SOURCE_URL}\""; \
        echo "corsika_config_version: \"${CORSIKA_CONFIG_VERSION}\""; \
        echo "corsika_config_revision: \"$(cat /workdir/simulation_software/corsika_config_revision)\""; \
        echo "corsika_config_source_url: \"${CORSIKA_CONFIG_SOURCE_URL}\""; \
        echo "autoconf_version: \"${AUTOCONF_VERSION}\""; \
        echo "autoconf_sha256: \"$(cat /workdir/simulation_software/autoconf_sha256)\""; \
        echo "avx_flag: \"${AVX_FLAG}\""; \
        echo "iact_atmo_version: \"${IACT_ATMO_VERSION}\""; \
        echo "variant:"; \
    } > "${CORSIKA_RUN_DIR}/build_opts.yml" && \
    for config_file in ../config_*.h; do \
        basename_file=$(basename "$config_file" .h); \
        corsika_name=$(echo "$basename_file" | sed 's/^config_/corsika_/'); \
        he_model=$(echo "$basename_file" | sed 's/^config_\([^_]*\)_.*/\1/'); \
        le_model=$(echo "$basename_file" | sed 's/^config_[^_]*_\([^_]*\)_.*/\1/'); \
        variant=$(echo "$basename_file" | sed 's/.*_\(curved\|flat\)$/\1/'); \
        hilow_default=$(cat "${CORSIKA_RUN_DIR}/.${corsika_name}.hilow_default"); \
        optimization="$AVX_FLAG"; \
        if [ "$AVX_FLAG" = "generic" ] || [ "$variant" = "curved" ]; then \
            optimization="generic"; \
        fi; \
        { \
            echo "  - executable: \"${corsika_name}\""; \
            echo "    config: \"${basename_file}\""; \
            echo "    atmosphere_geometry: \"${variant}\""; \
            echo "    he_hadronic_model: \"${he_model}\""; \
            echo "    le_hadronic_model: \"${le_model}\""; \
            echo "    hadronic_transition_energy_default_gev: ${hilow_default}"; \
            echo "    optimization: \"${optimization}\""; \
            echo "    cerenkopt: $([ "$optimization" = "generic" ] && echo false || echo true)"; \
            echo "    vlibm: $([ "$optimization" = "generic" ] && echo false || echo true)"; \
        } >> "${CORSIKA_RUN_DIR}/build_opts.yml"; \
        rm -f "${CORSIKA_RUN_DIR}/.${corsika_name}.hilow_default"; \
    done

# -----------------------------------------------------------------------------
# STAGE 3: Runtime
# -----------------------------------------------------------------------------
FROM ${BASE_IMAGE}
ARG CONTAINER_USER="simtools"
ARG CONTAINER_UID="1000"
ARG CONTAINER_GID="1000"
WORKDIR /workdir/simulation_software
COPY --from=build_image /workdir/corsika-run /workdir/simulation_software/corsika7

RUN microdnf update -y && microdnf install -y \
    libgfortran && microdnf clean all && \
    groupadd --gid "${CONTAINER_GID}" "${CONTAINER_USER}" && \
    useradd --uid "${CONTAINER_UID}" --gid "${CONTAINER_USER}" \
        --create-home --home-dir "/home/${CONTAINER_USER}" --shell /bin/bash "${CONTAINER_USER}" && \
    chown -R "${CONTAINER_UID}:${CONTAINER_GID}" /workdir "/home/${CONTAINER_USER}"

WORKDIR /workdir/simulation_software/corsika7
ENV HOME="/home/${CONTAINER_USER}"
# hadolint ignore=DL3066
USER ${CONTAINER_UID}:${CONTAINER_GID}
